题解:P9293 [ROI 2018] Addition without carry
Comentropy · · 题解
这个题出得真的好 qwq。
题意
稍加分析(或者题目名字已经很明显地说了),就能得出这个题的意思:对于
判定化问题
首先考虑判定一个特定的
从高到低位考虑,一个很直觉的贪心就是,让当前最高为
由于
正确性可以用调整法随便做做就可以得到了(真的不难,但是要严谨说明会比较繁琐,如果有时间会在结尾附上)。
上下界分析
注意这并没有单调性,所以我们得找到一种比较好的方法来确定范围。既然是最小化,那我们从最高位逐位确定。为了统一书写记号,我们让最低位从
假定答案最高位是
在此之前不一定有思路,我们先看看下界:
其中不同的矩形表示了不同的
而且我们再观察下界的图,如果我们取出那个使
流程、时间复杂度
哦,我们现在有一种非常好的描述方案步骤的方法了,而且可以通过搜索模拟出来:
- 找到目前使得
\lvert a_k\rvert +k-1 取到最大的最小的k (我们维护当前没有被消掉的\{a_i\} ,下标表示其排名); - 尝试下界
h_0 ,即:删除a_1,\dots,a_{k-1} ,让a_k 删掉自己的最高位后重新插入集合,递归判断现在的集合\{a\} 是否有解,如果失败,还原序列并进入下一步;否则我们确定了现在的最高位就是最优的,就得到了一组最优方案。 - 尝试
h_0+1 。注意,其实我们这个地方必然有解,正如同先前的图那样,把1 填满一段前缀就是一个解);但是我们有时没有必要只填前缀——我们可能有更好的方案,于是我们可以把[1,k] 全部删掉,接着递归判断。
- 注意,递归过程中我们需要记录一个值表示可以接受的最高位,这显然是有限制的。比如说第
2 步中我们递归下去的子问题得到答案的最高位不应当取到\lvert a_k\rvert 。
这个搜索过程看起来,每一步都可能搜两次,这不是指数级的吗?但是仔细一看,好像完全不可能跑满。我们来试着分析一下:
如果我们成功执行了步骤
我们更加知道一件事:每次递归下去,我们选中的那个
于是必然经过
前一半式子对应操作
过程中的序列操作不难,使用线段树(可以外加 set,但最好不要用【见代码部分说明】),维护
我们可能需要去掉某个
代码
以下是代码实现。
关于 set 的使用:由于笔者真的很弱,只能做分析做到这一步了,我不清楚在 这篇题解 中使用 set 每次逐个加入、删除的时间是不是对的,因为它与
但是在写这篇文章的时候,我写的使用 set 的解法是目前最优解第三,只跑了 1.04s,这里是提交记录:Submission。有感兴趣的老师可以 Hack 一下或者证明正确性。
我重新写了一份复杂度应当严格正确的线段树解法,特化成前缀删和区间查,还是比较好写的:
#include<bits/stdc++.h>
#define all(X) X.begin(),X.end()
using namespace std;
const int N=3e5+10;
const int INF=0x3f3f3f3f;
struct Node{
int v,p,c,tag;
};
inline Node operator+(const Node &X,const Node &Y){
return (Node){max(X.v,Y.v),X.v>=Y.v?X.p:Y.p,X.c+Y.c,0};
}
struct SegTree{
Node t[N<<2];
// int t[N<<2],p[N<<2],tag[N<<2];
#define ls (x<<1)
#define rs (x<<1|1)
inline void upd(int x,int y){t[x].v+=y,t[x].tag+=y;}
inline void pushup(int x){t[x]=t[ls]+t[rs];} // attention to eq.
inline void pushdown(int x){if(t[x].tag){upd(ls,t[x].tag),upd(rs,t[x].tag),t[x].tag=0;}}
void build(int x,int l,int r){
if(l==r) return t[x]={-INF,l,0,0},void();
int mid=(l+r)>>1; build(ls,l,mid),build(rs,mid+1,r);
pushup(x);
}
void update(int x,int l,int r,int ql,int qr,int y,int ad){ // ad 表示对单点次数的修改,主要是懒得改了,有点丑
if(ql<=l && r<=qr) return upd(x,y),t[x].c+=ad,void();
int mid=(l+r)>>1;pushdown(x);
if(ql<=mid) update(ls,l,mid,ql,qr,y,ad);
if(qr>mid) update(rs,mid+1,r,ql,qr,y,ad);
pushup(x);
}
Node query(int x,int l,int r,int ql,int qr){
if(ql<=l && r<=qr) return t[x];
int mid=(l+r)>>1;pushdown(x);
if(qr<=mid) return query(ls,l,mid,ql,qr);
if(ql>mid) return query(rs,mid+1,r,ql,qr);
return query(ls,l,mid,ql,qr)+query(rs,mid+1,r,ql,qr);
}
}w;
int n,prv[N],ans[N],a[N],ne[N],rk[N],tot=0; // ne 指向当前串中的下一个一的排名
string s[N];
vector<int> pos[N];
inline void upd(int x,int op){
w.update(1,1,tot,x,x,op*(INF+a[x]),op);
if(x<tot) w.update(1,1,tot,x+1,tot,op,0);
// 后面的数排名都加/减 1。
// 此处的排名指的是前面的数的个数,正是 i - 1
}
inline void printans(int l,int r,int x){
for(int i=l;i<=r;i++) ans[i]=x;
}
bool solve(int lim,int del){
if(del>tot) return true;
auto [mx,p,_c,_t]=w.query(1,1,tot,del,tot);
int c=w.query(1,1,tot,del,p).c;
if(mx<=0) return true;
if(mx>lim) return false;
w.update(1,1,tot,p+1,tot,-c,0);
if(ne[p]!=INF) upd(ne[p],1);
int b=mx-c+1; // b 就是 |a_i|, mx = max |a_i| + i - 1, tmp.size() == i
if(solve(b-1,p+1)) return printans(b,mx,1),true;
//
if(ne[p]!=INF) upd(ne[p],-1);
if(mx+1<=lim) // 事实上,如果这玩意成立那么下面肯定有解。
return assert(solve(b,p+1)),printans(b+1,mx+1,1),true;
w.update(1,1,tot,p+1,tot,c,0);
return false;
}
int main(){
ios::sync_with_stdio(false),cin.tie(0);
cin>>n;int mx=0;
for(int i=1,len;i<=n;i++){
cin>>s[i];len=s[i].size();
mx=max(mx,len),reverse(all(s[i])),prv[i]=INF; // add: prv init.
for(int j=len-1,las=-1;j>=0;j--)
if(s[i][j]=='1') pos[j].push_back(i),++tot;
}
for(int i=0,ts=tot;i<mx;i++){
sort(all(pos[i]),[&](int x,int y){return prv[x]>prv[y];});
for(int j:pos[i]){ // fixed: 呃,这里的遍历顺序是从小到大遍历的,上面也得先从小到大排 qwq。
ne[ts]=prv[j],prv[j]=ts,a[ts]=i+1;
rk[j]=ts--;
}
}
w.build(1,1,tot);
for(int i=1;i<=n;i++)
upd(rk[i],1);
solve(INF,1);
int ena=0;
for(int i=N-1;i>=1;i--)
if(ena|=ans[i]) putchar(ans[i]+'0');
if(!ena) putchar('0');
return 0;
}