Codeforces Round 893 (Div. 2) 解题报告
Codeforces Round 893 (Div. 2)
要开始复健解题报告了。
啊啊啊啊啊网站炸了比赛推了,又变回正常时间了
获得惩罚:睡觉时间 \(-1\operatorname{hour}\)
首次 Div. 2 打进前 100 名(可能也是最后一次),上了 master。
A. Buttons
显然两个人都会优先按都能按的。
那么如果 \(c\) 是奇数,A 就可以比 B 多按一次。
最后只要 A 按的比 B 多就是 first
。
时间复杂度 \(\Theta(1)\)。
代码实现
int a,b,c;
void Solve()
{
cin>>a>>b>>c;
if(a>b||(a==b&&(c&1)))puts("First");
else puts("Second");
}
B. The Walkway
吐槽题面丑。
定义 \(f(x,y)\) 表示从位置 \(x\) 开始走并在此时吃一块饼干,走到位置 \(\bm{y-1}\) 一共吃的饼干数。特别的,定义 \(f(x,x)=0\)。这个东西是好计算的。
那么,如果不移除任何小卖部,则总饼干数为
考虑移除第 \(x\) 个小卖部,则饼干数为
注意,此时 \(x=1\) 和 \(x=m\) 需要特判。代码中用了一点 trick 简化代码。
然后随便统计一下即可。时间复杂度 \(\Theta(m)\)。
代码实现
int n,m,k,a[100005];
int f(int x,int y)
{
return (a[y]-a[x]-1<0?0:1+(a[y]-a[x]-1)/k);
}
void Solve()
{
cin>>n>>m>>k;
a[0]=1,a[m+1]=n+1;
int sum=0;
for(int i=1;i<=m;i++)cin>>a[i];
int cnt=0,ans;
for(int i=0;i<=m;i++)sum+=f(i,i+1);
ans=sum;
for(int i=1;i<=m;i++)
{
int a2=sum-f(i,i+1)-f(i-1,i)+f(i-1,i+1);
if(a2<ans)ans=a2,cnt=1;
else if(a2==ans)cnt++;
}
cout<<ans<<" "<<cnt<<endl;
}
C. Yet Another Permutation Problem
吐槽撞题。U R wrong, that's Y.
显然任意两个数的 \(\gcd\) 不能超过 \(\left\lfloor\dfrac{n}{2}\right\rfloor\)。
下面给出一个达到理论上界的构造:
从大到小考虑每个 \(x\),如果 \(x\) 还未在排列中,则将 \(x,\dfrac{x}{2},\dfrac{x}{4},\dfrac{x}{8},\dots\) 依次加到排列中,直到变成了一个奇数为止。证明见原题。
时间复杂度 \(\Theta(n)\)。
代码实现
int n;
set<int>s;
void Solve()
{
cin>>n;
if(n&1)cout<<(n--)<<" ";
s.clear();
for(int i=n;i;i--)
{
if(s.count(i))continue;
int t=i;
while(!(t&1))cout<<t<<" ",s.insert(t),t>>=1;
cout<<t<<" ";
s.insert(t);
}
cout<<endl;
}
D. Trees and Segments
DP 好题。赛时没做出来。
记 \(pre_{i,j}\) 为前 \(i\) 个字符,最多修改 \(j\) 次,所能得到的最长连续的 \(0\) 的数量。同理定义 \(suf_{i,j}\),表示从 \(i\) 到末尾,其余同 \(pre\)。
然后这两个东西是好转移的。
考虑令最长连续 \(1\) 的区间为 \([l,r]\),我们可以求出需要修改的位置数量(假设为 \(t\))。那么此时,最长连续的 \(0\) 的数量就是 \(\max\{pre_{l-1,k-t},suf_{r+1,k-t}\}\),然后再去更新答案(当然如果 \(t>k\) 也就不用更新)。
直接这样做是 \(\Theta(n^3)\) 的(但是我代码假成 \(\Theta(n^3)\) 居然 \(\text{2800+ ms}\) 跑过了)。
注意到对于固定的长度,我们只需求出连续 \(0\) 的最大长度就可以了。然后对于相邻长度相等的两个区间,要修改的个数也只需要 \(\Theta(1)\) 转移。
于是我们枚举长度 \(len\),再扫一遍长度为 \(len\) 的所有区间(这只需要 \(\Theta(n)\)),可以求出此时连续 \(0\) 的最大长度。然后我们用其来更新答案即可。
总复杂度 \(\Theta(n^2)\)。听说还能优化到 \(\Theta(nk)\)。
多测不清空,WA 两行泪。
代码实现
const int N=3005;
int n,k,pre[N][N],suf[N][N],ans[N];
string st;
void Solve()
{
cin>>n>>k>>st;st="$"+st;
for(int i=1;i<=n;i++)
for(int j=0;j<=k;j++)
if(st[i]=='1')
{
if(j)pre[i][j]=pre[i-1][j-1]+1;
else pre[i][j]=0;
}
else pre[i][j]=pre[i-1][j]+1;
for(int i=1;i<=n;i++)
for(int j=0;j<=k;j++)
pre[i][j]=max(pre[i][j],max(pre[i-1][j],j?pre[i][j-1]:0));
for(int i=n;i;i--)
for(int j=0;j<=k;j++)
if(st[i]=='1')
{
if(j)suf[i][j]=suf[i+1][j-1]+1;
else suf[i][j]=0;
}
else suf[i][j]=suf[i+1][j]+1;
for(int i=n;i;i--)
for(int j=0;j<=k;j++)
suf[i][j]=max(suf[i][j],max(suf[i+1][j],j?suf[i][j-1]:0));
for(int i=0;i<=n;i++)
{
int d=-1,c=k;
for(int j=1;j<i;j++)if(st[j]=='0')c--;
for(int j=1;j<=n-i+1;j++)
{
if(st[j-1]=='0')c++;
if(st[j+i-1]=='0')c--;
if(c<0)continue;
d=max(d,max(pre[j-1][c],suf[j+i][c]));
}
if(~d)for(int j=1;j<=n;j++)ans[j]=max(ans[j],d*j+i);
}
for(int i=1;i<=n;i++)cout<<ans[i]<<sp_el(i,n);
}
E. Rollback
吐槽:好像 E1 和 E2 确实没啥关系。
离线做法(E1)
先给一个赛时的 E1 离线做法,不感兴趣也可以直接去看在线做法。
考虑建一棵类似 trie 的东西,加数的时候直接跳到相应儿子,删的时候直接跳 \(k\) 级祖先,同时用 vector
记录之前走过的节点用来回溯即可。
最后用 set
统计一下每个结点的答案,统一输出答案即可。时间复杂度 \(\Theta(n\log n)\)(\(\log\) 来自倍增跳 \(k\) 级祖先。)
但是 trie 没法直接存儿子,用 map
也会 MLE。这里给一个时间换空间的做法:
注意到只有加数的时候会建至多一个新节点,加数时我们可以直接建一个新点(无论父亲是否已经有这个儿子)。这样做,节点数仍然为 \(\Theta(n)\),而我们省去了 map
,于是更容易通过。
代码实现
(这是被 skipped 掉的一发,赛后再交同样 accepted,但是更好看。)
const int N=1000005;
struct node
{
vector<PII>sons;
int val;
int fa[20];
}tr[N];int cur;
int n,q;
char op;int x;
int now,p[N];
VI ops;
int new_calc(int x,int Fa)
{
++cur;
tr[Fa].sons.PB(x,cur);
tr[cur].fa[0]=Fa;
for(int i=1;;i++)
if(!(tr[cur].fa[i]=tr[tr[cur].fa[i-1]].fa[i-1]))return cur;
}
int grand(int x,int k)
{
for(int i=19;~i;i--)
if(k&(1<<i))x=tr[x].fa[i];
return x;
}
int cnt[N],siz;
void dfs(int x)
{
tr[x].val=siz;
for(PII son:tr[x].sons)
{
int v=son.first;
if(!cnt[v]++)siz++;
dfs(son.second);
if(!--cnt[v])siz--;
}
}
void Solve()
{
cin>>n;
now=cur=1;
ops.PB(1);
for(int i=1;i<=n;i++)
{
cin>>op;
if(op=='!')
{
ops.PPB();
now=ops.back();
continue;
}
if(op=='+'||op=='-')
{
cin>>x;
if(op=='+')now=new_calc(x,now);
if(op=='-')now=grand(now,x);
ops.PB(now);
}
else p[++q]=now;
}
dfs(1);
for(int i=1;i<=q;i++)cout<<tr[p[i]].val<<endl;
}
在线做法(E1+E2)
如果不需要回溯的话,暴力就可以均摊 \(\Theta(1)\)。回溯:万恶之源
那现在怎样卡暴力?加一堆数,全部删除,回溯,再全部删除,再回溯……
那现在怎样防止这件事发生?想想 dijkstra 咋从堆里删元素的?是的,懒惰删除。
我们记当前序列长度为 \(len\)。当删除时,我们只将 \(len\gets len-k\),而不需要实际上地删除。
那查询咋办?我们可以记录每个元素最早出现的位置,当查询的时候求出最早出现位置 \(\le len\) 的数的个数,用树状数组维护即可。至于最早出现位置,用 set
就可以维护。
至于回溯,记录上一次修改过的量,滚回去即可。
注意到对于加入、删除操作,要修改的元素(包括 \(len\) 与 \(a_i\))只有 \(\Theta(1)\) 个,完美。
总时间复杂度 \(\Theta(n\log n)\)(\(\log\) 来自树状数组和 set
。)
const int N=1000005;
int n,len,a[N];
vector<int>clen;
vector<PII>ca;
int c[N],b[N];
set<int>p[N];
void change(int pos,int val)
{
int d=val-b[pos];b[pos]=val;
while(pos<N)c[pos]+=d,pos+=pos&(-pos);
}
int sum(int pos)
{
int res=0;
while(pos)res+=c[pos],pos-=pos&(-pos);
return res;
}
void cg(int k,int x)
{
int y=a[k];a[k]=x;
if(y)
{
change(*p[y].begin(),0);
p[y].erase(k);
if(nonEmp(p[y]))change(*p[y].begin(),1);
}
if(x)
{
if(nonEmp(p[x]))change(*p[x].begin(),0);
p[x].insert(k);
change(*p[x].begin(),1);
}
}
void Solve()
{
cin>>n;
while(n--)
{
char op=' ';
while(op!='+'&&op!='-'&&op!='!'&&op!='?')op=getchar();
if(op=='?')printf("%d\n",sum(len)),fflush(stdout);
else if(op=='!')
{
len=clen.back();clen.PPB();
if(ca.back().first)
cg(ca.back().first,ca.back().second);
ca.PPB();
}
else
{
int x;scanf("%d",&x);
if(op=='+')
{
clen.PB(len++);
ca.PB(len,a[len]);
cg(len,x);
}
else clen.PB(len),len-=x,ca.PB(0,0);
}
}
}