Codeforces Round #666 (Div. 1) 题解

A

假设第三次操作范围是 [ 1 , n ] [1,n] [1,n],那么前两次操作的目标就是把每个数变成 n n n 的倍数。

假设前两次目标的操作范围都是 b b b,那么对于每个数 a a a,需要找到一个 x x x,满足 a + b x ≡ 0 ( m o d n ) a+bx\equiv 0\pmod n a+bx0(modn),即 x ≡ − a b x\equiv -\dfrac a b xba,那么就需要保证 b b b 在模 n n n 意义下有逆元,所以 b b b 需要是个质数。而两次操作要覆盖整个序列,所以 b > n 2 b>\dfrac n 2 b>2n,实现的时候我是选了小于 n n n 的最大质数。我这种笨比做法还需要特判 n ≤ 2 n\leq 2 n2 的情况,而且求逆元还需要写一个 exgcd \text{exgcd} exgcd,求质数需要写一个线性筛……

事实上,一个很好写的做法是,第一次操作 [ 1 , n − 1 ] [1,n-1] [1,n1],第二次操作 [ n , n ] [n,n] [n,n],第三次操作 [ 1 , n ] [1,n] [1,n],容易发现,第一次操作使每个数 a a a 加上 ( n − 1 ) a (n-1)a (n1)a,第二次随便搞,第三次操作时每个数都是 n n n 的倍数了。

我的笨比代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010
#define ll long long

int n;
ll a[maxn];
void exgcd(ll a,ll b,ll &x,ll &y){
    
    
	if(!b){
    
    x=1,y=0;return;}
	exgcd(b,a%b,y,x);
	y-=a/b*x;
}
int prime[maxn],t=0;
bool v[maxn];
void work()
{
    
    
	for(int i=2;i<=maxn-10;i++){
    
    
		if(!v[i])prime[++t]=i;
		for(int j=1;j<=t&&i*prime[j]<=maxn-10;j++){
    
    
			v[i*prime[j]]=true;
			if(i%prime[j]==0)break;
		}
	}
}

int main()
{
    
    
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	if(n==1){
    
    
		printf("1 1\n%lld\n1 1\n0\n1 1\n0",-a[1]);
	}else if(n==2){
    
    
		printf("1 1\n%lld\n2 2\n%lld\n1 1\n0",-a[1],-a[2]);
	}else{
    
    
		work();
		int len;
		for(int i=1;i<=t;i++)
		if(prime[i]<n)len=prime[i];
		else break;
		printf("1 %d\n",len);
		ll in,x,y;
		exgcd(len,n,x,y);
		in=(x%n+n)%n;
		for(int i=1;i<=len;i++){
    
    
			x=a[i]*in%n;x=(n-x)%n;
			printf("%lld ",x*len);
			a[i]+=x*len;
		}
		printf("\n%d %d\n",n-len+1,n);
		for(int i=n-len+1;i<=n;i++){
    
    
			x=a[i]*in%n;x=(n-x)%n;
			printf("%lld ",x*len);
			a[i]+=x*len;
		}
		printf("\n%d %d\n",1,n);
		for(int i=1;i<=n;i++)printf("%lld ",-a[i]);
	}
}

B

考虑能取完和不能取完两种情况。

假如有一堆石子比其他加起来都多,那么先手一直取这堆就赢了。

否则,判断所有石子总和的奇偶性,奇数则先手赢,偶数则后手赢,因为赢的那一方总是有办法取完所有石子,一个简单的策略就是一直取 当前能取的 最多的 那堆石子。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010

int T,n,a[maxn];

int main()
{
    
    
	scanf("%d",&T);while(T--)
	{
    
    
		scanf("%d",&n);
		int ma=0,sum=0;
		for(int i=1;i<=n;i++){
    
    
			scanf("%d",&a[i]);
			sum+=a[i];ma=max(a[i],ma);
		}
		sum-=ma;
		if(ma>sum)printf("T\n");
		else if((sum+ma)%2)printf("T\n");
		else printf("HL\n");
	}
}

C

每个关卡有两种打法,第一种一次打死boss,第二种两次打死boss。

第一种打法为:用手枪清完所有小怪,再用AWP打死boss;

第二种打法为:用手枪清完所有小怪,然后再用手枪点一下boss;或用一次激光枪。

第二种打法打完就要跑,然后还要回来再用手枪点死boss。

如果 i i i 用第二种打法,那么需要考虑下一关用不用第二种打法,如果用,那么最优打法就是 i    打 法 2 → i + 1    打 法 2 → i    点 死 b o s s → i + 1    点 死 b o s s i~~打法2\to i+1~~打法2\to i~~点死boss\to i+1~~点死boss i  2i+1  2i  bossi+1  boss,如果不用,那么最优打法就是跑了之后马上回来点死boss。

还需要考虑的特殊情况是, n − 1 n-1 n1 用第二种打法, n n n 用第一种打法,然后回到 n − 1 n-1 n1 点死boss,这种情况的结束位置在 n − 1 n-1 n1 而不是 n n n

我赛时还多考虑了一种情况,就是一个后缀除了 n n n 都用第二种打法,然后再从 n n n 往回点死所有boss,后来发现是多余的,并不会比上面的转移更优,手玩一下就知道了。

去掉多余情况的代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 2000010
#define ll long long

int n;
ll r1,r2,r3,d,a[maxn],f[maxn][2],g[maxn];
void chkmin(ll &x,ll y){
    
    if(y<x)x=y;}
ll ans=0,sum=0;

int main()
{
    
    
	scanf("%d %lld %lld %lld %lld",&n,&r1,&r2,&r3,&d);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	g[0]=0;
	for(int i=1;i<=n;i++){
    
    
		f[i][0]=f[i][1]=g[i]=1e18;
		chkmin(f[i][0],a[i]*r1+r3);
		chkmin(f[i][1],a[i]*r1+2*r1+d);
		chkmin(f[i][1],r2+r1+d);
		
		if(i>1)chkmin(g[i],g[i-2]+(i==2?0:d)+f[i-1][1]+f[i][1]+d);
		chkmin(g[i],g[i-1]+(i==1?0:d)+f[i][0]);
		chkmin(g[i],g[i-1]+(i==1?0:d)+f[i][1]+d);
	}
	printf("%lld",min(g[n],g[n-2]+(n==2?0:d)+f[n-1][1]+f[n][0]+d));
}

D

先将 y y y 坐标离散化,便于使用线段树维护,以及为了方便,不将M&M放在格子中间,将它放在格子的左下角。

然后从左往右枚举一个左端点,再从右往左枚举一个右端点,枚举右端点时,需要同时维护线段树,线段树第 i i i 位记录的是离散化后以 ( i − 1 , i ] (i-1,i] (i1,i] 作为矩形下界,以枚举的左右端点作为矩形左右边界,那么矩形上界至少是多高。这个东西有单调性,所以很好维护。

每次右移左端点时,先尺取法求一遍线段树的初值,然后右端点从右往左扫,每次左移后,要删除原来在右边界上的M&M,设它的坐标为 r r r,它下方离他最近的同色M&M y y y 坐标为 l l l,它上方离他最近的同色M&M y y y 坐标为 d d d,那么 ( l , r ] (l,r] (l,r] 区间的上界就要对 d d d max ⁡ \max max,维护上下最近的同色M&M用链表即可。

具体细节就看代码吧:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 2010
#define mod 1000000007

int n,k,L;
struct point{
    
    int x,y,col,ny;}a[maxn];
struct linked_list{
    
    
	int last[maxn],next[maxn];
	void del(int x){
    
    
		next[last[x]]=next[x];
		last[next[x]]=last[x];
	}
}s,S;
int last[maxn],sy[maxn];
bool cmpx(point x,point y){
    
    return x.x<y.x;}
bool cmpy(point x,point y){
    
    return x.y<y.y;}
int col[maxn],t[maxn],lim[maxn];
#define zuo ch[0]
#define you ch[1]
void add(int &x,int y){
    
    x=(x+y>=mod?x+y-mod:x+y);}
int add(int x){
    
    return x>=mod?x-mod:x;}
int dec(int x){
    
    return x<0?x+mod:x;}
struct node{
    
    
	int l,r,mid,z,mi,ma,lazy;node *ch[2];
	node(int x,int y):l(x),r(y),mid(l+r>>1),z(0),ma(0){
    
    
		if(x<y){
    
    
			zuo=new node(x,mid);
			you=new node(mid+1,y);
		}else zuo=you=NULL;
	}
	void update(int c){
    
    
		z=1ll*(sy[r]-sy[l-1])*c%mod;
		lazy=mi=ma=c;
	}
	void check(){
    
    
		z=add(zuo->z+you->z);
		mi=min(zuo->mi,you->mi);
		ma=max(zuo->ma,you->ma);
	}
	void init(){
    
    
		if(l==r)return update(lim[l]);
		zuo->init();you->init();lazy=0;check();
	}
	void change(int x,int c){
    
    
		if(mi>=c)return;
		if(ma<=c&&l==x)return update(c);
		if(lazy){
    
    
			if(zuo)zuo->update(lazy),you->update(lazy);
			lazy=0;
		}	
		if(x<=mid)zuo->change(x,c);
		you->change(max(mid+1,x),c);
		check();
	}
}*root=NULL;
int ans=0;

int main()
{
    
    
	scanf("%d %d %d",&n,&k,&L);
	for(int i=1;i<=n;i++)scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].col);
	sort(a+1,a+n+1,cmpy);a[0].x=a[0].y=-1;a[n+1].x=a[n+1].y=L;
	for(int i=0;i<=n+1;i++)sy[i]=a[i].y,a[i].ny=i;
	for(int i=1;i<=n;i++){
    
    
		int &p=last[a[i].col];
		if(p)s.next[p]=i,s.last[i]=p;
		s.next[i]=n+1;p=i;
	}
	sort(a+1,a+n+1,cmpx);
	root=new node(1,n);
	for(int i=1;i<=n;i++){
    
    
		if(a[i].x!=a[i-1].x){
    
    
			for(int j=1;j<i;j++)col[a[j].ny]=0;//尺取法预处理上界
			for(int j=i;j<=n;j++)col[a[j].ny]=a[j].col;
			for(int l=1,r=1,tot=0;l<=n;l++){
    
    
				for(;r<=n&&tot<k;r++)if(col[r]&&!t[col[r]]++)tot++;
				lim[l]=(tot<k?L:sy[r-1]);if(col[l]&&!--t[col[l]])tot--;
			}
			root->init();S=s;
			for(int j=n;j>=1;j--){
    
    
				add(ans,1ll*dec(1ll*(sy[n]+1)*L%mod-root->z)*(a[j+1].x-a[j].x)%mod*(a[i].x-a[i-1].x)%mod);
				int p=a[j].ny; root->change(S.last[p]+1,sy[S.next[p]]); S.del(p);
			}
		}
		s.del(a[i].ny);
	}
	printf("%d",ans);
}

E

先考虑求出最小贡献的匹配和最大贡献的匹配,最小是简单贪心,最大可以参考这里(务必要看,下面需要)。然后判断一下 k k k 在不在这个范围内。

然后从最大匹配开始考虑逐渐减小总贡献。可以发现,最大匹配只在重心进行匹配,假如在重心的一个儿子处进行一次匹配,然后剩下的再在重心进行匹配,那么总答案就会减 2 2 2

这样我们就得到了减少总贡献的方法:在自己子树内进行一些匹配,然后将剩下的点丢给父亲去处理。记 c [ i ] c[i] c[i] 表示 i i i 子树内有多少点丢给父亲处理,那么 i i i 子树内需要匹配的点数就是 ( ∑ y ∈ s o n c [ y ] ) − c [ i ] (\sum_{y\in son}c[y])-c[i] (ysonc[y])c[i]

问题就是怎么求 c c c 了,实际上就是不断减少父亲的匹配然后增加儿子的匹配,随手优化到 O ( n ) O(n) O(n) 就能过。

具体细节就看代码吧:

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 100010
#define ll long long
#define pb push_back

int n;ll k;
vector<int>e[maxn];
ll lower_limit=0,upper_limit=0;
int c[maxn],t[maxn],mson[maxn],rt=0;
void dfs1(int x,int fa){
    
    
	c[x]=1;mson[x]=0;
	for(int y:e[x])if(y!=fa){
    
    
		dfs1(y,x);
		c[x]+=c[y];
		mson[x]=max(mson[x],c[y]);
		lower_limit+=c[y]%2;
		upper_limit+=min(c[y],n-c[y]);
	}
	mson[x]=max(mson[x],n-c[x]);
	if(mson[x]<=mson[rt])rt=x;
}
void dfs2(int x,int fa){
    
    
	c[x]=1;
	for(int y:e[x])if(y!=fa){
    
    
		dfs2(y,x);
		c[x]+=c[y];
	}
	t[c[x]]++;
}
vector<int>d[maxn],q,ans1,ans2;
void dfs3(int x,int fa){
    
    
	int sz=1;for(int y:e[x])if(y!=fa)dfs3(y,x),sz+=c[y];
	q.clear();d[x].pb(x);q.pb(x);
	for(int y:e[x])if(y!=fa)q.pb(y);
	//这里配对时不能将儿子丢上来的点全部排列出来然后搞,否则当图为链时会被卡到n^2
	//不过做法是类似的,头尾各取mat个点,然后将尾的mat个点逆序输出即可
	//注意这一定是最优的构造方法,如果这种构造方法不能构造出解,那么就一定是无解
	int mat=(sz-c[x])/2,st=0,ed=q.size()-1;
	ans1.clear();ans2.clear();
	for(int i=1;i<=mat;i++){
    
    
		while(!d[q[st]].size())st++;
		ans1.pb(d[q[st]].back());d[q[st]].pop_back();
		while(!d[q[ed]].size())ed--;
		ans2.pb(d[q[ed]].back());d[q[ed]].pop_back();
	}
	for(int i=0;i<mat;i++)printf("%d %d\n",ans1[i],ans2[mat-i-1]);
	for(int i=1;i<q.size();i++){
    
    
		if(d[x].size()<d[q[i]].size())swap(d[x],d[q[i]]);
		for(int j:d[q[i]])d[x].pb(j);
	}
}

int main()
{
    
    
	scanf("%d %lld",&n,&k);
	for(int i=1,x,y;i<n;i++)scanf("%d %d",&x,&y),e[x].pb(y),e[y].pb(x);
	mson[0]=n+1;dfs1(1,0);
	if(k<lower_limit||k>upper_limit||k%2!=upper_limit%2){
    
    //由于每次变化量为2,所以k要和upper_limit的奇偶性相同
		printf("NO");
		return 0;
	}
	dfs2(rt,0);int p=n-1;
	for(;p>1;p--){
    
    //让最大的c减小2
		if(upper_limit-2*t[p]>=k){
    
    
			upper_limit-=2*t[p];
			t[p-2]+=t[p];
		}else break;
	}
	for(int i=1;i<=n;i++)c[i]=min(c[i],p-(p+c[i])%2);c[rt]=0;
	for(int i=1;i<=n;i++)if(c[i]==p&&upper_limit>k)c[i]-=2,upper_limit-=2;
	printf("YES\n");
	dfs3(rt,0);
}

代码里有一个有趣的地方,就是每次都取最大的 i i i 来减少 t [ i ] t[i] t[i],即每次都使最大的 c [ x ] c[x] c[x] 2 2 2,为什么要以这种方式来搞呢?

注意上面的构造方法,对于一个节点 x x x,将 ( ∑ y ∈ s o n c [ y ] ) + 1 (\sum_{y\in son}c[y])+1 (ysonc[y])+1 简写成 ∑ \sum (里面的 + 1 +1 +1 是算上 x x x 点),令需要匹配的点数记为 m = ∑ − c [ x ] m=\sum-c[x] m=c[x](即代码中的 mat \text{mat} mat 乘以 2 2 2)。

假如将所有儿子丢上来的点排成一个序列,最优配对中,两个匹配点在序列上的距离为 ∑ − m 2 \sum-\dfrac m 2 2m,假如存在一个儿子 y y y 满足 c [ y ] > ∑ − m 2 c[y]>\sum-\dfrac m 2 c[y]>2m,那么就会无解。

推一下,就是:
c [ y ] − 2 ∑ − m 2 > 0 c [ y ] − ∑ + c [ x ] 2 > 0 2 c [ y ] > ∑ + c [ x ] \begin{aligned} c[y]-\frac {2\sum-m} 2&>0\\ c[y]-\frac {\sum+c[x]} 2&>0\\ 2c[y]&>\sum+c[x] \end{aligned} c[y]22mc[y]2+c[x]2c[y]>0>0>+c[x]

∑ ′ = ∑ − c [ y ] \sum'=\sum-c[y] =c[y],代入得:
c [ y ] > ∑ ′ + c [ x ] c[y]>\sum{'}+c[x] c[y]>+c[x]

注意到 c [ x ] c[x] c[x] 一开始等于 ∑ ′ + c [ y ] \sum'+c[y] +c[y],且 c [ x ] c[x] c[x] 每次变化量为 2 2 2,所以 c [ x ] c[x] c[x] ∑ ′ + c [ y ] \sum'+c[y] +c[y] 的奇偶性相同,即 c [ y ] c[y] c[y] ∑ ′ + c [ x ] \sum'+c[x] +c[x] 的奇偶性相同。

那么在 c [ y ] c[y] c[y] 变得不合法的前一刻,一定满足 c [ y ] = ∑ ′ + c [ x ] c[y]=\sum'+c[x] c[y]=+c[x],然后一定是减少了一次 c [ x ] c[x] c[x],使得 c [ y ] > ∑ ′ + c [ x ] c[y]>\sum'+c[x] c[y]>+c[x]

但是注意到,如果 c [ y ] = ∑ ′ + c [ x ] c[y]=\sum'+c[x] c[y]=+c[x],即 c [ y ] − c [ x ] = ∑ ′ > 0 c[y]-c[x]=\sum'>0 c[y]c[x]=>0,此时 c [ y ] > c [ x ] c[y]>c[x] c[y]>c[x],存在比 c [ x ] c[x] c[x] 大的 c c c,不可能减少 c [ x ] c[x] c[x]

所以,每次减少最大的 c c c,可以保证不会出现无解的情况。

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/108333873
今日推荐