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\) 表示配对字符的位置):

\[f_i=\min(s_i+f_{i+1},f_{nxt_{i+1}}) \]

问题变成了快速比较两个字符串的字典序,肯定首选哈希求出最长公共前缀,可以主席树暴力维护,更好的方法是维护一个动态增加叶子的 \(\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("");
}
posted @ 2021-12-05 11:48  C202044zxy  阅读(641)  评论(0编辑  收藏  举报