Codeforces Global Round 11

E. Xum

题目描述

一开始黑板上写了一个奇数 \(x\),每次操作可以选取黑板上的两个数,把他们的和或者异或和写在黑板上,试在 \(10^5\) 次操作内使得黑板上出现 \(1\),并且要保证任意时刻黑板上的数都不超过 \(5\cdot 10^{18}\)

\(3\leq x\leq 10^6\)

解法

发现不可能由加法直接得到 \(1\),得到 \(1\) 只能通过异或操作,那么求出一组偶数和偶数加一即可。

因为一开始给出的是奇数 \(x\),所以偶数可以通过 \(kx\)\(k\) 是偶数)得到,那么偶数加一就可以转化成模 \(x\) 余一。

这个模 \(x\) 余一的数肯定是一个数的若干倍,设它是 \(z\)\(p\) 倍,那么根据逆元的有关性质只要 \(z\)\(x\) 互质那么就一定存在一个合法的 \(p\),所以问题变成了找一个和 \(x\) 互质的数,然后解方程 \(pz=1\bmod x\) 即可。

找到这个和 \(x\) 互质的数肯定要用异或,假设 \(x\)\(1001\) 这种形式,那么我们把最低位平移到和最高位对齐,也就是得到 \(1001000\),把它们异或得到 \(1000001\),可以证明这个数一定和 \(x\) 互质,因为 \((x+2^kx-2^k)\) 一定和 \(x\) 互质。

把上面我讲的东西倒着模拟一遍就可以做出本题。

#include <cstdio>
#define int long long
const int M = 1005;
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 x,z,m,lx,lz,px[M],pz[M],a[M],b[M],c[M];
int gcd(int a,int b)
{
	return !b?a:gcd(b,a%b);
}
int exgcd(int a,int b,int &x,int &y)
{
	if(!b) {x=1;y=0;return a;}
	int g=exgcd(b,a%b,y,x);
	y-=x*(a/b);
	return y; 
}
signed main()
{
	x=z=read();px[0]=x;
	for(lx=0;px[lx]<=1e18;lx++)
	{
		px[lx+1]=px[lx]+px[lx];
		m++;a[m]=b[m]=px[lx];
	}
	while(1)
	{
		if(gcd(z^x,x)==1)
		{
			m++;a[m]=x;b[m]=z;c[m]=1;
			z=x^z;break;
		}
		m++;a[m]=b[m]=z;z+=z;
	}
	pz[0]=z;
	for(lz=0;pz[lz]<=1e18;lz++)
	{
		pz[lz+1]=pz[lz]+pz[lz];
		m++;a[m]=b[m]=pz[lz];
	}
	int k=0,t=0;exgcd(z,x,k,t);
	k=(k%x+x)%x;
	if(k%2==0) k+=x;
	k--;
	for(int i=lz;i>=0;i--)
		if(k&(1ll<<i))
		{
			m++;a[m]=z;b[m]=pz[i];
			z+=pz[i];
		}
	t=(z-1)/x;t--;
	for(int i=lx;i>=0;i--)
		if(t&(1ll<<i))
		{
			m++;a[m]=x;b[m]=px[i];
			x+=px[i];
		}
	m++;a[m]=z;b[m]=x;c[m]=1;
	printf("%lld\n",m);
	for(int i=1;i<=m;i++)
	{
		if(c[i]==0) printf("%lld + %lld\n",a[i],b[i]);
		else printf("%lld ^ %lld\n",a[i],b[i]);
	}
}

F. Boring Card Game

题目描述

点此看题

解法

简单结构的高级运用,真的让我看到了结构之美。

首先考虑一个简化版的问题,如果不要求两人顺序操作怎么办(还是不会做),你发现这是一个暴力消去连续三个相同元素的过程,可以用栈解决,也就是向栈顶塞元素,如果同种元素塞满三个那么弹栈顶,最后栈为空。

你发现栈的作用大概是如果中间出现了连续的三个相同元素,那么把他们消去后,前后的元素还能接起来,再考虑它们是否能被消去。那么此时就存在一个先后顺序的问题,我们把元素每三个一组分组,那么会得到一个形如这样的限制:消去某一组之后才能消去另一组

考虑用图论来表达这个性质,如果消去 \(A\) 组才能消去 \(B\) 组,那么 \(A\)\(B\) 连边。考虑在做栈的时候把这个图构建出来,也就是在栈顶出栈的时候,把这个栈顶向新的栈顶连边,我们发现最后得到的结构是一个森林

再思考题目保证了本题有解,思考有解的必要条件是有一个叶子属于第一个人,那么题目一定保证了这个条件。通过这个条件我们可以推出一定有一个根属于第二个人,可以给出这样的构造方案:

  • 轮到第一个人操作的时候,因为有一个根属于第二个人,所以一定能找到一个叶子,删除它即可。
  • 轮到第二个人操作的时候,由于属于第二个人的点要多一个,所以一定能找到一个叶子,但是注意如果只剩一个根并且不是最后一步就不能删根,只能删其他叶子,要不然第一个人的构造会出问题。

那么最后一定能删干净,把上述过程模拟一遍就可以得到删边方案。

总结

从简化问题入手,首先要能主动提出简化问题,然后找简化问题和原问题之间差了什么,把这点想清楚。

数据结构的图论模型很重要,这道题就是利用了栈的潜在树结构。

#include <cstdio>
const int M = 10005;
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,a[M],b[M],c[M],d[M];
int s[M],fa[M],p[M][3],vis[M];
signed main()
{
	n=read();
	for(int i=1;i<=3*n;i++)
		a[read()]=1;
	for(int i=1;i<=6*n;i++)
	{
		if(!m || b[s[m]]!=a[i])
		//insert the stack and create a node
		{
			b[++k]=a[i];
			s[++m]=k;p[k][c[k]++]=i;
		}
		else
		{
			p[s[m]][c[s[m]]++]=i;
			if(c[s[m]]==3)//pop the stack
				fa[s[m]]=s[m-1],m--;
		}
	}
	int c=1,cnt=0;
	for(int i=1;i<=k;i++) d[fa[i]]++;
	for(int i=1;i<=k;i++) if(!fa[i] && !b[i]) cnt++;
	for(int i=1;i<=k;i++)
	{
		int x=0;
		for(int j=1;j<=k;j++)
			if(!d[j] && b[j]==c && !vis[j])
			{
				if(i<k && cnt==1 && c==0 && !fa[j]) continue;
				//must have at least one root
				x=j;
			}
		d[fa[x]]--;c^=1;vis[x]=1;
		if(b[x]==0 && !fa[x]) cnt--;
		printf("%d %d %d\n",p[x][0],p[x][1],p[x][2]);
	}
}

G.One Billion Shades of Grey

题目描述

点此看题

解法

首先有一个明显的 \(\tt observation\):新填入的数值只可能是边界上的数值,应该可以用调整法证明

提出简化版问题:考虑边界上只有两种数值 \(x\)\(y\),那么可以转化成一个染色问题,不同色的点要记录一次代价。那这不是一个显然的最小割模型么?如果边界上的点是 \(x\),那么把它连 \(S\),否则把它连 \(T\),相邻的点之间连一条流量为 \(1\) 的无向边。

然后考虑有多种颜色的原问题,如果要套最小割的话那么考虑相邻两个颜色求一次最小割,对于边界上的点如果权值大于等于 \(v_{i+1}\) 那么连 \(T\),如果小于等于 \(v_i\) 那么连 \(S\),如果两个相邻点的权值是 \(s_x,s_y(x<y)\) 的话,那么会被拆分成 \(s_y-s_x=(s_y-s_{y-1})+(s_{y-1}-s_{y-2})...(s_{x+1}-s_x)\) 被统计。

但是这里有一个逻辑漏洞,由于每次是独立求最小割的,那么对于两个点可能出现 \(s_x<k_1,s_y>k_1\)\(s_x>k_2,s_y<k_2\) 都被局部最优解统计了,但这显然出现了矛盾,非要这么做的话就要证明不会出现这种情况。

Lemma:对于最小割中任意被划分到了 \(T\) 的点,在把某个点 \(u\) 由连 \(T\) 变为连 \(S\) 之后,新的最小割中这些点不会被划分到 \(S\) 中。官方题解中有较为严谨的证明,这里给出我的感性证明:

考虑把一个集合 \(s\) 调整由染 \(T\) 调整成染 \(S\),那么在不考虑和 \(u\) 连的边时尚且不优(因为上一次没有这样做),那么考虑了和 \(u\) 连的边割只会增加,所以最小割中不会出现此类情况。

那么每次就可以单独做了,但是如果我们暴力跑的话好像会超时欸\(...\)

由于流量是 \(O(n)\) 级别的,那么跑一次的复杂度是 \(O(n^3)\),因为每次只会把一个点由 \(T\) 改到 \(S\),把经过这个点的流暴力撤回是 \(O(n^2)\) 的,一共需要撤回 \(n\) 次所以总时间复杂度 \(O(n^3)\),实现用暴力增广的方法即可。

总结

考虑简化问题还是很重要啊\(...\)

如果知道一个局部问题的最优解,可以通过构造答案上界的方法来获取全局最优解。

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
#define ll long long
#define pii pair<int,int>
#define mp make_pair
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],a[205][205],vis[M],is[M],it[M];
ll ans,flow;vector<pii> v;
struct edge
{
	int v,c,next;
}e[2*M];
int id(int x,int y)
{
	return (x-1)*n+y;
}
void add(int u,int v)
{
	e[++tot]=edge{v,0,f[u]},f[u]=tot;
	e[++tot]=edge{u,0,f[v]},f[v]=tot;
}
int dfs(int u,int fl)
{
	if(it[u]==1 && fl==1) return 1;//belong to T
	if(is[u]==1 && fl==-1) return 1;//belong to S
	if(vis[u]) return 0;
	vis[u]=1;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(fl==1 && e[i].c==1) continue;//enlarge 
		if(fl==-1 && e[i].c!=-1) continue;//roll back
		if(vis[v]) continue;
		if(dfs(v,fl))
		{
			e[i].c++;e[i^1].c--;
			return 1;
		}
	}
	return 0;
}
signed main()
{
	n=read();tot=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			a[i][j]=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		{
			if(a[i][j]<0) continue;
			if(j<n && a[i][j+1]>=0)
				add(id(i,j),id(i,j+1));
			if(i<n && a[i+1][j]>=0)
				add(id(i,j),id(i+1,j));
			if(a[i][j]>0) it[id(i,j)]=1,
				v.push_back(mp(a[i][j],id(i,j)));
		}
	sort(v.begin(),v.end());
	for(int i=0;i+1<v.size();i++)
	{
		int u=v[i].second;
		memset(vis,0,sizeof vis);
		while(dfs(u,-1))
		{
			flow--;
			memset(vis,0,sizeof vis);
		}
		it[u]=0;is[u]=1;
		memset(vis,0,sizeof vis);
		for(int j=0;j<=i;j++)
			while(dfs(v[j].second,1))
			{
				flow++;
				memset(vis,0,sizeof vis);
			}
		ans+=flow*(v[i+1].first-v[i].first);
	}
	printf("%lld\n",ans);
}
posted @ 2021-08-20 12:13  C202044zxy  阅读(134)  评论(0编辑  收藏  举报