Codeforces Global Round 17
\(\tt noip\) 之后的第一场线上赛,感觉手感退化了很多啊,不知道上红的目标能不能如期实现呢?
D. Not Quite Lee
题目描述
数轴上有 \(n\) 个窗口,第 \(i\) 个窗口的长度为 \(b_i\)(包含这么多连续的整数),定义一个窗口的权值为包含数字的和,问有多少个窗口的子序列满足存在一种滑动方案使得权值和为 \(0\)
\(n\leq 2\cdot 10^5\)
解法
考虑调整法,一开始可以取总和为 \(\sum \frac{c_i(c_i-1)}{2}\) 的窗口组合,那么滑动相当于把权值 \(+c_i\) 或者 \(-c_i\),那么合法的充要条件是存在序列 \(\{x_i\}\) 满足 \(\sum c_i\cdot x_i=\sum\frac{c_i(c_i-1)}{2}\)
根据裴蜀定理可以转化为 \(\sum\frac{c_i(c_i-1)}{2}|\gcd(c_1,c_2...c_n)\),但是好像还是不可做。
我们观察 \(\sum\frac{c_i(c_i-1)}{2}\) 有什么性质,其中特殊的是 \(2\) 这个常数,这提示我们可以着重讨论奇偶性。
然后观察到如果原序列中存在奇数那么一定合法,因为此时一定满足 \(\sum \frac{c_i(c_i-1)}{2}|\gcd\),尝试扩展这个观察,也就是把每个子序列在满足 \(\gcd|2^l\) 的最大的 \(l\) 处统计。
对于 \(l>0\),我们把限制拆成 \(\sum\frac{c_i(c_i-1)}{2}|2^l\and \sum\frac{c_i(c_i-1)}{2}|\frac{g}{2^l}\),也就是满足 \(c_i|2^l\and c_i\not| 2^{l+1}\) 的有偶数个并且至少有 \(1\) 个,所以可以用容斥原理简单计算。
#include <cstdio>
const int M = 200005;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,a[M],pw[M];
signed main()
{
n=read();pw[0]=1;
for(int i=1;i<=n;i++)
{
int x=read(),cnt=0;
while(x%2==0) cnt++,x/=2;
a[cnt]++;
pw[i]=pw[i-1]*2%MOD;
}
int ans=pw[n]-pw[n-a[0]],y=n-a[0];
for(int i=1;i<=30;i++)
{
int x=y;y-=a[i];
if(x-1<=y) continue;
ans+=pw[x-1]-pw[y];
}
printf("%lld\n",(ans%MOD+MOD)%MOD);
}
E. AmShZ and G.O.A.T.
题目描述
定义一个序列为坏当且仅当严格大于平均数的数量 大于 严格小于平均数的数量。
问最少删除多少个元素使得最后的序列的任何子序列都不为坏。
\(n\leq 2\cdot 10^5\),保证原序列单增。
解法
考虑转化判据,原序列的任何子序列不为坏当且仅当原序列的任何一个长度为 \(3\) 的子序列不为坏,必要性显然,下证充分性:
考虑对于如果 \(a_{i+1}-a_i<a_i-a_1\),那么 \(\{1,i,i+1\}\) 就是一组坏的序列。否则我们可以知道对于任何一个子序列都有 \(c_{\lceil\frac{k}{2}\rceil}\leq AVG\)(因为是凸函数,中间位置一定在下方),这足以说明不存在坏的子序列。
就上面这个简单的证明我证了一周,当然是边学文化课有空余时间再证的
那么我们只需要保证 \(a_{i+1}-a_i\geq a_i-a_1\) 就可以得到好的序列,考虑一个一个加数,那么新数与首项的距离每次一定翻倍,所以在非常数序列的情况下,序列长度是 \(O(\log a)\) 的。
所以我们枚举首项,然后贪心地找最近合法的下一项,注意特判相等的情况,那么时间复杂度 \(O(n\log n\log a)\)
总结
本题的难点其实是结论,这种类型就是判断最基本的情况就有了充分性。
证明方法难以用语言表达,自己体会一下吧
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 200005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,ans,a[M];
signed main()
{
T=read();
while(T--)
{
n=read();ans=0;
for(int i=1;i<=n;i++)
a[i]=read();
for(int i=1;i<=n;i++)
{
if(a[i]==a[i-1]) continue;
int j=i,res=1;
while(j<=n)
{
j=lower_bound(a+j+1,
a+n+1,2*a[j]-a[i])-a;
if(j<=n) res++;
}
ans=max(ans,res);
}
printf("%d\n",n-ans);
}
}
G. AmShZ Wins a Bet
题目描述
解法
虽然评分虚高但还是做不来,但是补这种题还是多有意思的🐱👤
经过我的尝试发现贪心是不行的,有一个关键的 \(\tt observation\):如果删除一对 ()
,那么其中的字符必须要全部删除,要不然字典序不会变小,所以只需要使用相邻 ()
的删除操作就可以得到最优解。
这说明我们可以把问题转化成保留原序列的若干连续段,使得剩下的串字典序最小。
显然这是一个简单的线性 \(dp\) 模型,考虑到字典序的特性我们从后往前 \(dp\),设 \(f_i\) 表示操作后 \(i\) 个字符留下来的字典序最小的串,可以在 \(f_{i+1}\) 的基础上直接添加,还可以找到和当前的 (
在原串上配对 )
的位置,然后删除这一整段(根据结论这是唯一需要考虑的),所以可以得到(设 \(nxt_i\) 表示配对字符的位置):
问题变成了快速比较两个字符串的字典序,肯定首选哈希求出最长公共前缀,可以主席树暴力维护,更好的方法是维护一个动态增加叶子的 \(\tt trie\) 树,用树上倍增的方法跳最长公共前缀(如果哈希值相同就往上跳)
时间复杂度 \(O(n\log n)\),注意 \(\tt trie\) 树上尽量不要重复开节点要不然容易乱套。
总结
字典序问题贪心不是唯一解,倒序 \(dp\) 同样充分利用了字典序的性质。
把复杂的问题转化成简单的 \(dp\) 模型,本题就是先证明只需要使用连续段就可以转线性 \(dp\)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 300005;
#define ull unsigned long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,cnt,q[M],f[M],ch[M][2],fa[M][20],nxt[M];
ull dp[M][20],pw[M]={1};char s[M];
void ins(int p,int c)
{
if(ch[p][c]) return ;
int x=++cnt;ch[p][c]=x;
fa[x][0]=p;dp[x][0]=c;
for(int i=1;i<=19;i++)
{
int to=fa[x][i-1];
fa[x][i]=fa[to][i-1];
dp[x][i]=pw[1<<i-1]*dp[to][i-1]+dp[x][i-1];
}
}
int cmp(int x,int y)//string x < string y is ture ?
{
for(int i=19;i>=0;i--)
if(fa[x][i] && fa[y][i] && dp[x][i]==dp[y][i])
x=fa[x][i],y=fa[y][i];
if(x==1) return 1;
if(y==1) return 0;
return dp[x][0]<dp[y][0];
}
signed main()
{
scanf("%s",s+1),n=strlen(s+1);
for(int i=1;i<=n;i++)
pw[i]=pw[i-1]*371;
cnt=f[n+1]=1;
for(int i=n;i>=1;i--)
{
if(s[i]=='(' && m) nxt[i]=q[m--];
if(s[i]==')') q[++m]=i;
if(s[i]==')')//add it dirctly
{
ins(f[i+1],1);f[i]=ch[f[i+1]][1];
continue;
}
ins(f[i+1],0);int to=ch[f[i+1]][0];
if(!nxt[i] || cmp(to,f[nxt[i]+1])) f[i]=to;
else f[i]=f[nxt[i]+1];
}
int nw=f[1];
while(nw!=1)
{
if(dp[nw][0]==0) printf("(");
else printf(")");
nw=fa[nw][0];
}
puts("");
}
H.Squid Game
题目描述
解法
事实证明如果调不出来一定要拍,而且要用强力的 datamaker
来拍。
首先考虑对于最优选点方案,我们以被选取点的一个点为根建树,我们可以在脑海中想象枚举根的过程,但在下面的讨论中我们不妨以 \(1\) 为根建树。
那么在选取了 \(1\) 之后我们发现所有 祖先-后代
类型的要求(要求一)是没有被满足的,但是 \(\tt lca\) 不是端点的要求(要求二)是都被满足了的,所有我们只需要解决要求一。
到这一步我们可以直接使用延迟贪心,也就是我们做 \(\tt dfs\),维护子树内未完成的要求。如果有一种要求不能上传给父亲,那么证明必须选取这个点,注意从 \(v_1\) 的要求上传到 \(u\),如果 \(v_2\) 子树内有点被选取,那么 \(v_1\) 上传的要求是不需要考虑的,用 \(\tt set\) 维护可以做到 \(O(n\log^2 n)\)
但是如果我们暴力枚举根复杂度爆炸,因为根只带来了 \(1\) 的影响,我们尝试用讨论的方法解决它。如果此时要求二已经全部被解决了,那么我们就不需要选取 \(1\),此时答案一定最优;如果此时要求二没有全部被满足,我们证明此时选取 \(1\) 是必要的,因为我们的延迟贪心是在最浅的位置放置,那么没被满足的要求二的 \(\tt lca\) 一定在更浅的地方,所以说单独处理是必要的。
总结
路径问题可以考虑定根,根的作用有:作为路径的起点;解决掉若干情况。
较小的影响可以通过讨论法解决,可以通过证明必要性来说明操作的最优性。
#include <cstdio>
#include <vector>
#include <iostream>
#include <set>
using namespace std;
const int M = 300005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,tot,ans,f[M],a[M],b[M],c[M],d[M],fa[M][20];
set<int> s[M];vector<int> o[M];
struct edge
{
int v,next;
}e[M];
void dfs0(int u,int p)
{
d[u]=d[p]+1;fa[u][0]=p;
for(int i=1;i<=19;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==p) continue;
dfs0(v,u);
}
}
int lca(int u,int v)
{
if(d[u]<d[v]) swap(u,v);
for(int i=19;i>=0;i--)
if(d[fa[u][i]]>=d[v])
u=fa[u][i];
if(u==v) return u;
for(int i=19;i>=0;i--)
if(fa[u][i]^fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
void dfs1(int u,int p)
{
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==p) continue;
dfs1(v,u);
c[u]+=c[v];
}
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==p) continue;
if(c[u]-c[v]) continue;
if(s[u].size()<s[v].size())
swap(s[u],s[v]);
for(auto x:s[v]) s[u].insert(x);
}
if(s[u].size() && *s[u].rbegin()==d[u]-1)
c[u]++,ans++,s[u].clear();
for(auto x:o[u]) s[u].insert(x);
}
signed main()
{
n=read();m=read();
for(int i=2;i<=n;i++)
{
int j=read();
e[++tot]=edge{i,f[j]},f[j]=tot;
}
dfs0(1,0);
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),x=lca(u,v);
if(u==fa[v][0] || v==fa[u][0])
{
puts("-1");
return 0;
}
if(x==v) o[u].push_back(d[v]);
else if(x==u) o[v].push_back(d[u]);
else a[++k]=u,b[k]=v;
}
dfs1(1,0);
for(int i=1;i<=k;i++)
if(ans-c[a[i]]-c[b[i]]==0)
{
ans++;
break;
}
printf("%d\n",ans);
}
I. Mashtali vs AtCoder
题目描述
给定一棵 \(n\) 个点的数,然后分别固定 \(1,2...k\) 来做 \(n\) 次游戏。每次游戏的规则是:删除一条边,如果此时存在一个连通块内不包含固定点,那么直接删去这个连通块,不能操作者败。
你需要对这 \(n\) 次游戏分别求出是先手必胜还是先手必败。
\(n\leq 3\cdot 10^5\)
解法
首先我们考虑 \(k=1\) 的情况,它就是这道题的弱化版 Game on tree
通过打表发现子树 \(u\) 的 \(SG\) 值等于所有儿子 \(v\) 的 \(SG+1\) 的异或和,证明:
如果 \(u\) 有 \(k\) 个儿子,那么我们把 \(u\) 复制 \(k\) 份,那么我们得到了根节点只有一个儿子的独立子游戏,根的 \(SG\) 就等于这些独立子游戏的 \(SG\) 异或和,可以归纳证明根节点只有一个儿子的游戏的 \(SG\) 为儿子的 \(SG+1\)
对于两个节点的情况显然成立,如果我们删除根节点的边那么 \(SG=0\),否则归纳可知转移到的所有子状态的 \(SG\) 都是原来的 \(SG+1\),把这个过程放在 \(\tt mex\) 上考虑你就发现根的 \(SG\) 值是原来的 \(SG+1\)
对于 \(k>1\) 的情况,游戏的 \(SG\) 为:把前 \(k\) 个节点的虚树缩成一个点,得到根的 \(SG\) 值异或上虚树边数的奇偶性。证明我还没懂,如果读者会请不吝赐教。
那么我们以 \(1\) 为根建树,添加一个点就把到根路径上的所有边加入即可,时间复杂度 \(O(n)\)
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 300005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,tot,f[M],p[M],dp[M],vis[M];
struct edge
{
int v,next;
}e[2*M];
void dfs(int u,int fa)
{
p[u]=fa;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa) continue;
dfs(v,u);
dp[u]^=dp[v]+1;
}
}
signed main()
{
n=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
e[++tot]=edge{v,f[u]},f[u]=tot;
e[++tot]=edge{u,f[v]},f[v]=tot;
}
dfs(1,0);
int cur=dp[1];
printf("%d",(cur)?1:2);
for(int i=2;i<=n;i++)
{
vector<int> d;
for(int x=i;p[x] && !vis[x];x=p[x])
vis[x]=1,d.push_back(x);
for(auto x:d)
{
cur^=dp[x]+1;
cur^=dp[x];
cur^=1;
}
printf("%d",(cur)?1:2);
}
puts("");
}