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);
}