【AtCoder】AtCoder Grand Contest 018 解题报告
A:Getting Difference(点此看题面)
- 给定一个大小为\(n\)的集合。
- 你可以进行任意次操作,每次任选集合中两个数,将它们差的绝对值加入集合。
- 问能否使\(k\)出现在集合中。
- \(n\le10^5,k\le10^9\)。
签到题
考虑这类似于一个辗转相减的过程。
因此我们先将给定序列排序去重,求出相邻元素之差的\(gcd\),然后看看是否有一个\(a_i-k\)是\(gcd\)的倍数就好了。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
using namespace std;
int n,k,a[N+5];
I int gcd(CI x,CI y) {return y?gcd(y,x%y):x;}
int main()
{
RI i,t;for(scanf("%d%d",&n,&k),i=1;i<=n;++i) scanf("%d",a+i);
sort(a+1,a+n+1),n=unique(a+1,a+n+1)-a-1;//排序去重
if(n==1) return puts(a[1]^k?"IMPOSSIBLE":"POSSIBLE"),0;//特判只有一个数
for(t=a[2]-a[1],i=2;i^n;++i) t=gcd(t,a[i+1]-a[i]);//求出相邻两数之差gcd
for(i=1;i<=n;++i) if(a[i]>=k&&!((a[i]-k)%t)) return puts("POSSIBLE"),0;//看是否有一个数减若干gcd能得到k
return puts("IMPOSSIBLE"),0;
}
B:Sports Festival(点此看题面)
- 有\(n\)个人和\(m\)个活动,已知每个活动在每个人心目中的排名。
- 你需要选择举行任意个活动,每个人都会选择举行的活动里心目中排名最高的参加。
- 求参加人数最多的活动的最小人数。
- \(n\le300\)
二分答案
考虑我们二分答案\(x\),并在一开始假设举行所有活动。
那么对于一个人数超过\(x\)的活动,我们必须把它给去掉,因为不管如何去掉其他活动也不可能使这个活动人数减少。
而如果把所有活动都去完了,显然不合法。
代码:\(O(n^3logn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 300
using namespace std;
int n,m,a[N+5][N+5],p[N+5],s[N+5],tg[N+5];
I bool Check(CI x)//检验
{
RI i,j,k;for(i=1;i<=m;++i) s[i]=tg[i]=0;for(i=1;i<=n;++i) ++s[a[i][p[i]=1]];//初始化
RI t=0;for(i=1;i<=m;++i) for(j=1;j<=m;++j) if(s[j]>x)//操作m轮保证操作完全,如果有一个活动人数超过x就需要删去
{
if(++t==m) return false;for(tg[j]=k=1;k<=n;++k)//找到所有参加这个活动的人
if(a[k][p[k]]==j) {--s[j];W(tg[a[k][++p[k]]]);++s[a[k][p[k]]];}//选择下一个活动
}return true;
}
int main()
{
RI i,j;for(scanf("%d%d",&n,&m),i=1;i<=n;++i) for(j=1;j<=m;++j) scanf("%d",a[i]+j);
RI l=1,r=n,mid;W(l<r) Check(mid=l+r>>1)?r=mid:l=mid+1;return printf("%d\n",r),0;//二分答案
}
C:Coins(点此看题面)
- 有\(X+Y+Z\)个人,每个人有\(a_i\)个金币,\(b_i\)个银币,\(c_i\)个铜币。
- 你可以不重复地获得\(X\)个人的金币、\(Y\)个人的 银币、\(Z\)个人的铜币。
- 求获得的最多的总币数。
- \(X+Y+Z\le10^5\)
模拟费用流?
感觉这道题费用流是很好建图的,所以讲道理应该可以用模拟费用流去做。
但我实在太菜了,根本推不来模拟费用流做法。。。
只有金币和银币
考虑只有金币和银币,显然如果获得了\(i\)的金币或\(j\)的银币,就说明\(a_i+b_j\ge b_i+a_j\)(否则完全可以交换)。
移项得到\(a_i-b_i\ge a_j-b_j\),所以我们可以把每个人按\(a_i-b_i\)排序,那么就是选择前\(X\)个人的金币,后\(Y\)个人的银币。
现在加上了铜币
我们依然把人按\(a_i-b_i\)排序,依然可以保证选金币的人和选银币的人之间有一条分界线。
于是我们去枚举分界线,分界线两侧分别变成了只有金币和铜币、只有银币和铜币。
仍采用先前类似的策略,只要从前往后、从后往前做两次,分别考虑两个问题,每次开一个堆维护一下\(a-c/b-c\)的前\(X/Y\)大值就好了。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define LL long long
using namespace std;
int X,Y,Z;LL l[N+5],r[N+5];priority_queue<int,vector<int>,greater<int> > Q;
struct Data
{
int x,y,z;I bool operator < (Con Data& o) Con {return x-y>o.x-o.y;}//按x-y排序
}s[N+5];
int main()
{
RI i;for(scanf("%d%d%d",&X,&Y,&Z),i=1;i<=X+Y+Z;++i) scanf("%d%d%d",&s[i].x,&s[i].y,&s[i].z);
for(sort(s+1,s+X+Y+Z+1),i=1;i<=X;++i) l[X]+=s[i].x,Q.push(s[i].x-s[i].z);//从前往后,金币和铜币
for(i=X+1;i<=X+Z;++i) s[i].x-s[i].z>Q.top()?//判断当前选金币更优是否更优
(l[i]=l[i-1]+s[i].x-Q.top(),Q.pop(),Q.push(s[i].x-s[i].z),0):(l[i]=l[i-1]+s[i].z);
W(!Q.empty()) Q.pop();for(i=X+Y+Z;i>X+Z;--i) r[X+Z+1]+=s[i].y,Q.push(s[i].y-s[i].z);//从后往前,银币和铜币
for(i=X+Z;i>X;--i) s[i].y-s[i].z>Q.top()?//判断当前选银币是否更优
(r[i]=r[i+1]+s[i].y-Q.top(),Q.pop(),Q.push(s[i].y-s[i].z),0):(r[i]=r[i+1]+s[i].z);
LL ans=0;for(i=X;i<=X+Z;++i) ans=max(ans,l[i]+r[i+1]);return printf("%lld\n",ans),0;//枚举分界线统计答案
}
D:Tree and Hamilton Path(点此看题面)
- 给定一棵\(n\)个点的树,每条边有一个边权。
- 以两点树上路径为边权建一张完全图,求最长的哈密顿路径长度。
- \(n\le10^5\)
考虑上界
考虑每一条边的贡献,它被经过的次数上界就是\(2\times \min\{num_x,num_y\}\),即两边点数较小值的两倍。
但显然不是所有边都能卡到上界的,且最优情况下应该只有一条边经过次数恰好比次数上界少\(1\)。
但这条卡不到上界的边显然不能随意指定。
如果存在一条边满足两边点数相同,那么这条边必然无法卡到上界。
否则,对于任意一个所有子节点子树大小不超过\(\frac n2\)的点,它的任意一条边都可以不卡到上界。
那么只要求一下每个点子节点子树大小最大值,稍微讨论一下就行了。
代码:\(O(n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define add(x,y,z) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].v=z)
using namespace std;
int n,ee,lnk[N+5];long long ans;struct edge {int to,nxt,v;}e[N<<1];
int val[N+5],Sz[N+5],Mx[N+5];I void dfs(CI x,CI lst)//遍历树,求子节点子树大小最大值
{
Sz[x]=1;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&
(
val[e[i].to]=e[i].v,val[x]=min(val[x],e[i].v),dfs(e[i].to,x),
ans+=2LL*e[i].v*min(Sz[e[i].to],n-Sz[e[i].to]),Sz[x]+=Sz[e[i].to],Mx[x]=max(Mx[x],Sz[e[i].to])//ans统计上界
);Mx[x]=max(Mx[x],n-Sz[x]);
for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&2*Sz[e[i].to]==n&&(val[x]=e[i].v);//如果存在两边点数相同的边
if(2*Sz[x]==n) for(RI i=lnk[x];i;i=e[i].nxt) e[i].to==lst&&(val[x]=e[i].v);//如果存在两边点数相同的边
}
int main()
{
RI i,x,y,z;for(scanf("%d",&n),i=1;i^n;++i) scanf("%d%d%d",&x,&y,&z),add(x,y,z),add(y,x,z);
RI res=1e9;val[1]=1e9,dfs(1,0);for(i=1;i<=n;++i) 2*Mx[i]<=n&&(res=min(res,val[i]));//贪心选择能选的边权最小的边
return printf("%lld\n",ans-res),0;//输出答案
}
E:Sightseeing Plan(点此看题面)
- 在一张网格图上划出三个矩形,满足第二个矩形在第一个矩形右下方,第三个矩形在第二个矩形右下方。
- 每次可以往下走一个单位或者往右走一个单位。
- 求从第一个矩形选一点出发,走到第二个矩形中选一点休息,最后走到第三个矩形结束行走的方案数。
- 坐标\(\le10^6\)
点到点
这应该是众所周知的,假设两点水平距离为\(x\),竖直距离为\(y\),那么方案数就是\(C_{x+y}^x\)。
方便起见,我们接下来设这个东西为\(S(x,y)\)。
点到矩形
考虑我们不直接用组合数,而是用另一种方式表示\(S(x,y)\)。
我们用\(x-i\)表示最后一次连续向右走了几步,用\(y-j\)表示在此之前最后一次连续向下走了几步,发现:
容易发现这是一个二维前缀和的形式。
而我们要求的是:
那么用经典的容斥,就是:
矩形到矩形
发现矩形到矩形就是第一个矩形的四个关键点到第二个矩形的方案数,也就是第一个矩形四个关键点到第二个矩形四个关键点的方案数。
直接\(4\times4\)暴枚。
矩形到矩形到矩形
发现从第一个矩形进入第二个矩形,要么从左边界进入,要么从上边界进入。
可能的进入点规模是\(O(n)\)的,我们完全可以暴枚。
同理要么从右边界出去,要么从下边界出去。
方案数的统计
考虑题目中求的并不单纯是路径数,还要考虑在第二个矩形中点的选择。
也就是说,每条路径的贡献应该是它在第二个矩形中的长度。
假设进入点为\((a_1,b_1)\),离开点为\((a_2,b_2)\),则这个长度就是\(a_2+b_2-a_1-b_1+1\)。
发现这个式子完全可以拆成两部分,分别在处理进入点和离开点时统计。
代码:\(O(16n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000000
#define X 1000000007
#define C(x,y) (1LL*Fac[x]*IFac[y]%X*IFac[(x)-(y)]%X)//组合数
#define S(x1,y1,x2,y2) C((x2)-(x1)+(y2)-(y1),(x2)-(x1))//从(x1,y1)到(x2,y2)的方案数
using namespace std;
int n,Fac[N+5],IFac[N+5],x[8],y[8];
struct node {int x,y,v;I node(CI a=0,CI b=0,CI c=0):x(a),y(b),v(c){}}p[10];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
I int Calc(Con node& A,Con node& B)//计算贡献
{
RI i,t=0;for(i=x[3];i<=x[4];++i)//枚举行
t=(t-1LL*S(A.x,A.y,i,y[3]-1)*S(i,y[3],B.x,B.y)%X*(i+y[3])%X+X)%X,//进入点
t=(t+1LL*S(A.x,A.y,i,y[4])*S(i,y[4]+1,B.x,B.y)%X*(i+y[4]+1)%X)%X;//离开点
for(i=y[3];i<=y[4];++i)//枚举列
t=(t-1LL*S(A.x,A.y,x[3]-1,i)*S(x[3],i,B.x,B.y)%X*(x[3]+i)%X+X)%X,//进入点
t=(t+1LL*S(A.x,A.y,x[4],i)*S(x[4]+1,i,B.x,B.y)%X*(x[4]+i+1)%X)%X;//离开点
return (A.v*B.v*t+X)%X;//注意乘上容斥系数
}
int main()
{
RI i,j;for(Fac[0]=i=1;i<=N;++i) Fac[i]=1LL*Fac[i-1]*i%X;//预处理阶乘
for(IFac[N]=QP(Fac[N],X-2),i=N;i;--i) IFac[i-1]=1LL*IFac[i]*i%X;//预处理阶乘逆元
for(i=1;i<=6;++i) scanf("%d",x+i);for(i=1;i<=6;++i) scanf("%d",y+i);
p[1]=node(x[1]-1,y[1]-1,1),p[2]=node(x[1]-1,y[2],-1),p[3]=node(x[2],y[1]-1,-1),
p[4]=node(x[2],y[2],1),p[5]=node(x[5],y[5],1),p[6]=node(x[5],y[6]+1,-1),
p[7]=node(x[6]+1,y[5],-1),p[8]=node(x[6]+1,y[6]+1,1);//找出第一个和第三个矩形共八个关键点
RI ans=0;for(i=1;i<=4;++i) for(j=5;j<=8;++j) ans=(ans+Calc(p[i],p[j]))%X;//暴枚关键点对
return printf("%d\n",ans),0;
}
F:Two Trees(点此看题面)
- 有两棵\(n\)个点的树,标号都为\(1\sim n\)。
- 你需要给两棵树中标号相同的点一个相同的权值,要求满足两棵树中任意子树的子树和都是\(1\)或\(-1\)。
- 求构造一组合法方案。
- \(n\le10^5\)
题解
先考虑如何判无解,发现就是某一个点在两棵树中子节点个数的奇偶性不同。
其实我们可以只用\(0,±1\)三种数来赋值。
我们可以先让每个点的儿子两两匹配。
如果一个节点有奇数个子节点,它的权值是\(0\),而它的子树和就取决于剩下的那个儿子的子树和。
匹配在具体实现中就是在一张新的图上连边。
最后只要黑白染色一遍就行了(显然这是一张二分图)。
代码:\(O(n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
using namespace std;
int n,f[N+5];
struct Graph
{
int ee,lnk[N+5],c[N+5];struct edge {int to,nxt;}e[4*N+5];
I void add(CI x,CI y) {e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y;}
I void Paint(CI x)//黑白染色
{
for(RI i=lnk[x];i;i=e[i].nxt) !c[e[i].to]&&(c[e[i].to]=-c[x],Paint(e[i].to),0);//相邻点染成另一种颜色
}
}G;
struct Tree
{
int ee,lnk[N+5],c[N+5];struct edge {int to,nxt;}e[N+5];
I void add(CI x,CI y) {e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y;}
I void dfs(CI x)//遍历树
{
RI pre=0;for(RI i=lnk[x];i;i=e[i].nxt) dfs(e[i].to),
pre?(G.add(f[e[i].to],f[pre]),G.add(f[pre],f[e[i].to]),pre=0):(pre=e[i].to);//两两匹配
pre?(f[x]=f[pre]):(c[x]=1);//子节点个数为奇数时取决于剩下的那个子节点
}
}TA,TB;
int main()
{
RI i,x,A,B;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&x),~x?(TA.add(x,i),0):(A=i);
for(i=1;i<=n;++i) scanf("%d",&x),~x?(TB.add(x,i),0):(B=i);
for(i=1;i<=n;++i) f[i]=i;TA.dfs(A);for(i=1;i<=n;++i) f[i]=i;TB.dfs(B);
for(i=1;i<=n;++i) if(TA.c[i]^TB.c[i]) return puts("IMPOSSIBLE"),0;//判无解
for(i=1;i<=n;++i) TA.c[i]&&!G.c[i]&&(G.c[i]=1,G.Paint(i),0);//染色
for(puts("POSSIBLE"),i=1;i<=n;++i) printf("%d%c",G.c[i]," \n"[i==n]);return 0;//输出方案
}