Codeforces Round #721 div.2 A-E题解

视频讲解:BV1n54y1L7Nv

A. And Then There Were K

题目大意

给定整数 n ( 1 ≤ n ≤ 1 0 9 ) n(1 \leq n \leq 10^9) n(1n109) ,求最大的整数 k k k ,使得
n & ( n − 1 ) & ( n − 2 ) & ( n − 3 ) & . . . ( k ) = 0 n\&(n-1)\&(n-2)\&(n-3)\&...(k)=0 n&(n1)&(n2)&(n3)&...(k)=0

题解

一般遇到这种题,可以考虑打表找规律,然后发现小于 n n n 的最大的 2 m − 1 2^m-1 2m1 即是答案。
具体证明如下:

  • 为使得 n n n 中的二进制最高位的 1 1 1 在与运算后消除,则答案应满足 k ≤ 2 m − 1 k \leq 2^m-1 k2m1
  • 因为 ( 2 m ) & ( 2 m − 1 ) = 0 (2^m) \& (2^m-1)=0 (2m)&(2m1)=0 ,因此 k = 2 m − 1 k=2^m-1 k=2m1 满足题意要求。

参考代码

#include<bits/stdc++.h>
using namespace std;

int main()
{
	int T,n,ans;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		ans=1;
		while(n>1)
		{
			n>>=1;
			ans<<=1;
		}
		ans--;
		printf("%d\n",ans);
	}
}

B. Palindrome Game

题目大意

给定一个长度为 n ( 1 ≤ n ≤ 1 0 3 ) n(1 \leq n \leq 10^3) n(1n103) 01 01 01 字符串,Alice和Bob双方论轮流修改,每次修改可以进行以下操作之一:

  1. 将一个 0 0 0 修改为 1 1 1 ,花费 1 美元;
  2. 反转整个字符串,花费 0 0 0 美元。执行这个操作需要当前字符串为非回文串,且上一步不是反转操作;

当所有字符都变为 1 1 1 是,游戏结束。花费最少的玩家获得胜利。双方都会采用最优策略,Alice先手行动。给定字符串,求Alice胜,还是Bob胜,或是平手。

本题有两个版本。Easy版本中给定字符串为回文串,Hard版本中给定字符串不一定是回文串。

题解

本题有两种解法,一种是直接通过贪心策略手工推导结论,另一种是用博弈论思想跑动态规划。

动态规划解法

d p s a m e , d i f f , m i d , r e v dp_{same,diff,mid,rev} dpsame,diff,mid,rev 表示在双方选择最优策略的情况下,局面为 ( s a m e , d i f f , m i d , r e v ) (same,diff,mid,rev) (same,diff,mid,rev) 时当前选手与对手的花费差,其中:

  • s a m e same same :表示对称的 0 0 0 的对数,即满足 0 ≤ i ≤ n 2 − 1 0 \leq i \leq \frac{n}{2}-1 0i2n1 a i = a n − i − 1 = 0 a_i=a_{n-i-1}=0 ai=ani1=0 i i i 数量;
  • d i f f diff diff :表示不对称的 0 0 0 的数量,即满足 0 ≤ i ≤ n 0 \leq i \leq n 0in a i = 0 ≠ a n − i − 1 a_i = 0 \neq a_{n-i-1} ai=0=ani1 i i i 数量;
  • m i d mid mid :表示字符串长度是否为奇数且 a n 2 = 0 a_{\frac{n}{2}}=0 a2n=0
  • r e v rev rev :表示上一步是否为翻转操作

那么当前可执行的操作及转移式分别为:

  • d i f f > 0 diff>0 diff>0 r e v = 0 rev=0 rev=0 ,可执行反转操作,花费 0 0 0 d p s a m e , d i f f , m i d , r e v = − d p s a m e , d i f f , m i d , 1 dp_{same,diff,mid,rev}=-dp_{same,diff,mid,1} dpsame,diff,mid,rev=dpsame,diff,mid,1
  • s a m e > 0 same>0 same>0 ,可以将一个对称的 0 0 0 改为 1 1 1 ,花费 1 1 1 d p s a m e , d i f f , m i d , r e v = 1 − d p s a m e − 1 , d i f f + 1 , m i d , 0 dp_{same,diff,mid,rev}=1-dp_{same-1,diff+1,mid,0} dpsame,diff,mid,rev=1dpsame1,diff+1,mid,0
  • d i f f > 0 diff>0 diff>0 ,可以将一个不对称的 0 0 0 改为 1 1 1 ,花费 1 1 1 d p s a m e , d i f f , m i d , r e v = 1 − d p s a m e , d i f f − 1 , m i d , 0 dp_{same,diff,mid,rev}=1-dp_{same,diff-1,mid,0} dpsame,diff,mid,rev=1dpsame,diff1,mid,0
  • m i d > 0 mid>0 mid>0 ,可以将中间的 0 0 0 改为 1 1 1 ,花费 1 1 1 d p s a m e , d i f f , m i d , r e v = 1 − d p s a m e , d i f f , 0 , 0 dp_{same,diff,mid,rev}=1-dp_{same,diff,0,0} dpsame,diff,mid,rev=1dpsame,diff,0,0

找出上述符合条件的最小值,即为当前的 d p s a m e , d i f f , m i d , r e v dp_{same,diff,mid,rev} dpsame,diff,mid,rev 值。
若初始状态的 d p s a m e , d i f f , m i d , r e v dp_{same,diff,mid,rev} dpsame,diff,mid,rev > 0 >0 >0 则表示先手Alice花费更多,Bob胜; < 0 <0 <0 则Alice胜; = 0 =0 =0 则平手。

参考代码(动态规划解法)
#include<bits/stdc++.h>
using namespace std;

const int MAXN=1000;
int dp[MAXN>>1][MAXN>>1][2][2],vis[MAXN>>1][MAXN>>1][2][2];
char s[MAXN];

int dfs(int sam,int dif,int mid,int rev)
{
	if(vis[sam][dif][mid][rev])
		return dp[sam][dif][mid][rev];
	int ret=1<<29;
	if(dif&&rev==0)
		ret=min(ret,-dfs(sam,dif,mid,1));
	if(sam)
		ret=min(ret,1-dfs(sam-1,dif+1,mid,0));
	if(mid)
		ret=min(ret,1-dfs(sam,dif,0,0));
	if(dif)
		ret=min(ret,1-dfs(sam,dif-1,mid,0));
	vis[sam][dif][mid][rev]=1;
	return dp[sam][dif][mid][rev]=ret;
}

int main()
{
	int T,n,sam,dif,mid,i,ans;
	dp[0][0][0][0]=0;
	vis[0][0][0][0]=1;
	dp[0][0][0][1]=0;
	vis[0][0][0][1]=1;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		scanf("%s",&s);
		sam=dif=0;
		if(n%2==1&&s[n/2]=='0')
			mid=1;
		else
			mid=0;
		for(i=0;i<=n/2-1;i++)
		{
			if(s[i]==s[n-1-i]&&s[i]=='0')
				sam++;
			else if(s[i]=='0'||s[n-1-i]=='0')
				dif++;
		}
		ans=dfs(sam,dif,mid,0);
		if(ans>0)
			printf("BOB\n");
		else if(ans==0)
			printf("DRAW\n");
		else
			printf("ALICE\n");
	}
}
贪心解法

对于Easy版本,即输入字符串为回文串,有以下结论:

  • 0 0 0 的数量为偶数,则后手Bob胜。因为当Alice将 a i = 0 a_i=0 ai=0 变为 a i = 1 a_i=1 ai=1 后,Bob也可以跟着将对称的 a n − i − 1 = 0 a_{n-i-1}=0 ani1=0 变为 a n − i − 1 = 1 a_{n-i-1}=1 ani1=1 ,这样Bob花费最多和Alice一样。然而对于最后一对 a i = a n − i − 1 = 0 a_i=a_{n-i-1}=0 ai=ani1=0 ,Bob可以在Alice操作后,执行翻转操作,这样Bob花费就必定比Alice少 2 2 2
  • 0 0 0 的数量为1,则后手Bob胜。因为Alice只能花费 1 1 1 将中间的 0 0 0 变为 1 1 1,然后游戏结束,总和Alice花费比Bob多 1 1 1
  • 0 0 0 的数量为大于 1 1 1 的奇数,则先手Alice胜。因为Alice可以将中间的 a n 2 = 0 a_{\frac{n}{2}}=0 a2n=0 变为 a n 2 = 1 a_{\frac{n}{2}}=1 a2n=1 ,这样就轮到Bob面对 0 0 0 的数量为偶数这花费比对手多 2 2 2 局面了,总和Alice花费比Bob少 1 1 1

对于Hard版本,即输入字符串不一定回文串,有以下结论:

  • 若字符串为回文串,则与Easy版本一样;
  • 若字符串不为回文串,则Alice胜或平手。因为Alice可以通过不断执行翻转操作,迫使Bob执行修改操作,直到字符串差一步变为具有偶数个 0 0 0 的回文串。此时Alice花费 1 1 1 将其变为偶数个 0 0 0 ,即Bob将面对后续花费比Alice多 2 2 2 的局面。这样Bob至少比Alice多花费 1 1 1。某些情况下,Alice会一直翻转,例如 0011 0011 0011 。只有当字符串有一个居中 0 0 0 且另有一个不对称的 0 0 0 时,双方平手。

C. Sequence Pair Weight

题目大意

定义一个序列的权值,为这个序列中满足 i < j i < j i<j a i = a j a_i=a_j ai=aj ( i , j ) (i,j) (i,j) 对数。

给定一个长度为 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \leq n \leq 10^5) n(1n105) 的序列 a a a ,求序列 a a a 的所有子序列的权值和。

如果可以通过在序列 a a a 的开始与结尾处中删除若干个元素得到序列 b b b ,则称序列 b b b 是序列 a a a 的子序列。

题解

考虑如果存在一队 ( i , j ) (i,j) (i,j) ,满足 i < j i < j i<j a i = a j a_i=a_j ai=aj ,那么有多少子序列包含这对 ( i , j ) (i,j) (i,j) ,就是其对答案产生的贡献。

设子序列开始与结束位置分别为 l l l r r r ,易得当 1 ≤ l ≤ r 1 \leq l \leq r 1lr j ≤ r ≤ n j \leq r \leq n jrn 时,子序列 a [ l , r ] a[l,r] a[l,r] 包含点对 ( i , j ) (i,j) (i,j) ,这样的子序列有 i ∗ ( n − j + 1 ) i*(n-j+1) i(nj+1) 个。

因此我们可以从左到右扫描一遍序列,并统计更新每种元素各自的下标值和 ∑ i \sum{i} i , 乘上 ( n − j + 1 ) (n-j+1) (nj+1) 计入答案即可。具体的计算式为:
a n s = ∑ j = 1 n ( ( n − j + 1 ) ∗ ∑ i < j & a i = a j i ) ans=\sum_{j=1}^{n} ((n-j+1) * \sum_{i<j \& a_i=a_j}{i}) ans=j=1n((nj+1)i<j&ai=aji)

另外由于 a i a_i ai 很大,因此统计前需要离散化或者直接用map。

参考代码

#include<bits/stdc++.h>
using namespace std;

const int MAXN=100100;
int a[MAXN],cp[MAXN];
long long num[MAXN];
map<int,int> mp;

int main()
{
	int T,n,i,len;
	long long ans;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		for(i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			cp[i]=a[i];
		}
		sort(cp+1,cp+n+1);
		len=unique(cp+1,cp+n+1)-cp-1;
		mp.clear();
		for(i=1;i<=len;i++)
		{
			mp[cp[i]]=i;
			num[i]=0;
		}
		ans=0;
		for(i=1;i<=n;i++)
		{
			a[i]=mp[a[i]];
			ans+=(n-i+1)*num[a[i]];
			num[a[i]]+=i;
		}
		printf("%lld\n",ans);
	}
}

D. MEX Tree

题目大意

给定一棵具有 n ( 2 ≤ n ≤ 2 ⋅ 1 0 5 ) n(2 \leq n \leq 2 \cdot 10^5) n(2n2105) 个节点的树,节点编号从 0 0 0 n − 1 n-1 n1

对于 [ 0 , n ] [0,n] [0,n] 范围内的每一个整数 k k k ,求有多少个无序点对 ( u , v ) , u ≠ v (u,v),u \neq v (u,v)u=v ,满足从 u u u v v v 路径上所有节点的 M E X MEX MEX 值为 k k k

一个序列的 M E X MEX MEX 值为不属于这个序列的最小非负整数。

题解

如果要使得一条路径上节点的 M E X MEX MEX 值为 k k k ,则路径上必须包含 [ 0 , k − 1 ] [0, k-1] [0,k1] 范围内的所有节点且不包含 k k k

直接求解的话会发现不包含 k k k 这个条件比较难解决,但是可以考虑用差分的方法解决:

  • M E X MEX MEX k k k 的路径数 = = = 包含 [ 0 , k − 1 ] [0, k-1] [0,k1] 所有节点的路径数 − - 包含 [ 0 , k ] [0, k] [0,k] 所有节点的路径数

那么只要求解出对于任意 k ( 0 ≤ k ≤ n ) k(0 \leq k \leq n) k(0kn) ,包含 [ 0 , k ] [0,k] [0,k] 的路径数即可。

考虑维护这样的路径。
一开始时,求包含 [ 0 , 0 ] [0,0] [0,0] 的路径,不妨将 0 0 0 当作根节点跑一遍dfs,统计每个节点为根的子树大小,那么经过 0 0 0 的路径数可以直接统计出。
接下来考虑包含 [ 0 , 1 ] [0,1] [0,1] 的路径,如下图所示,等于红色节点数乘蓝色节点数。

接下来考虑包含 [ 0 , 2 ] [0,2] [0,2] 的路径。这时有以下几种可能性:

  1. 2 2 2 1 1 1 的子树中,则缩小蓝色范围即可;
  2. 1 1 1 2 2 2 的子树中,无需修改;
  3. 2 2 2 在与 1 1 1 不同的以 0 0 0 的儿子为根的子树中,则缩小红色部分即可;
  4. 2 2 2 在与 1 1 1 相同的以 0 0 0 的儿子为根的子树中,但 2 2 2 不在 1 1 1 子树中, 1 1 1 也不在 2 2 2 的子树中,则不存在这样的路径。

在这里插入图片描述

接下来考虑包含 [ 0 , 3 ] [0,3] [0,3] 的路径,这时候多了四种情况:

  1. 1 1 1 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 3 3 3 2 2 2 的子树中,则修改红色范围;
  2. 1 1 1 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 3 3 3 1 1 1 的子树中,则修改蓝色范围;
  3. 1 1 1 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 1 1 1 3 3 3 的子树中,或 2 2 2 3 3 3 的子树中,则无需修改;
  4. 1 1 1 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 3 3 3 不在 1 1 1 2 2 2 的子树中, 3 3 3 的子树也不包含 1 1 1 2 2 2,则不存在这样的路径;

对于后续节点,也是如上操作。最后输出答案前差分下结果即可。

参考代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=200200;
vector<int> vec[MAXN];
int cnt;
int dfn[MAXN],ed[MAXN];
long long siz[MAXN],ans[MAXN];

void dfs(int x,int fa)
{
	siz[x]=1;
	dfn[x]=++cnt;
	for(int i=0;i<vec[x].size();i++)
	{
		int son=vec[x][i];
		if(son==fa)
			continue;
		dfs(son,x);
		siz[x]+=siz[son];
	}
	ed[x]=cnt;
}

bool isParent(int x,int fa)
{
	return dfn[fa]<=dfn[x]&&dfn[x]<=ed[fa];
}

int main()
{
	int T,n,i,u,v,son,x,A,B,flag;
	long long all;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		for(i=0;i<n;i++)
			vec[i].clear();
		for(i=1;i<n;i++)
		{
			scanf("%d%d",&u,&v);
			vec[u].push_back(v);
			vec[v].push_back(u);
		}
		cnt=0;
		dfs(0,-1);
		all=1ll*n*(n-1)/2;
		ans[0]=0;
		for(i=0;i<vec[0].size();i++)
		{
			son=vec[0][i];
			ans[0]+=siz[son]*(siz[son]-1)/2;
			if(isParent(1,son))
				x=son;
		}
		A=1;
		B=0;
		flag=1;
		ans[1]=all-siz[A]*(n-siz[x]);
		for(i=2;i<n;i++)
		{
			ans[i]=all;
			if(!flag)
				continue;
			if(isParent(i,A))
				A=i;
			else if(!isParent(A,i))
			{
				if(!B)
				{
					if(isParent(i,x))
						flag=0;
					else
						B=i;
				}
				else
				{
					if(isParent(i,B))
						B=i;
					else if(!isParent(B,i))
						flag=0;
				}
			}
			if(flag)
			{
				if(!B)
					ans[i]-=siz[A]*(n-siz[x]);
				else
					ans[i]-=siz[A]*siz[B];
			}
		}
		ans[n]=all;
		for(i=n;i>=1;i--)
			ans[i]-=ans[i-1];
		for(i=0;i<=n;i++)
			printf("%lld ",ans[i]);
		puts("");
	}
}

E. Partition Game

题目大意

定义数组 t t t 的花费为:
c o s t ( t ) = ∑ x ∈ s e t ( t ) l a s t ( x ) − f i r s t ( x ) cost(t)=\sum_{x \in set(t)}{last(x)-first(x)} cost(t)=xset(t)last(x)first(x)

其中 s e t ( t ) set(t) set(t) t t t 中所有值去重后构成的集合, f i r s t ( x ) first(x) first(x) l a s t ( x ) last(x) last(x) t t t 中第一次和最后一次出现 x x x 的位置。

给定长度为 n ( 1 ≤ n ≤ 35000 ) n(1 \leq n \leq 35000) n(1n35000) 的数组 a a a ,将其分为 m ( 1 ≤ m ≤ m i n ( n , 100 ) ) m(1 \leq m \leq min(n,100)) m(1mmin(n,100)) 个连续段,求每段花费之和的最小值。

题解

d p i , j dp_{i,j} dpi,j 表示前 i i i 个数分为 j j j 段的最小花费,易得
d p i , j = m i n k < i { d p k , j − 1 + c o s t ( k + 1 , i ) } dp_{i,j}=min_{k<i} \{ dp_{k,j-1} +cost(k+1,i) \} dpi,j=mink<i{dpk,j1+cost(k+1,i)}

直接求解的复杂度是 O ( N 2 k ) O(N^2k) O(N2k) ,超时,考虑数据结构优化。
考虑比较 i i i 增大时 c o s t cost cost 的变化。对比 c o s t ( k + 1 , i − 1 ) cost(k+1,i-1) cost(k+1,i1) c o s t ( k + 1 , i ) cost(k+1,i) cost(k+1,i) ,可能有以下变化:

  1. a [ k + 1 , i − 1 ] a[k+1,i-1] a[k+1,i1] 范围内没有 a i a_i ai ,则 c o s t ( k + 1 , i ) = c o s t ( k + 1 , i − 1 ) cost(k+1,i)=cost(k+1,i-1) cost(k+1,i)=cost(k+1,i1)
  2. a [ k + 1 , i − 1 ] a[k+1,i-1] a[k+1,i1] 范围内存在 a i a_i ai ,则 c o s t ( k + 1 , i ) = c o s t ( k + 1 , i − 1 ) + i − b e f a i cost(k+1,i)=cost(k+1,i-1)+i-bef_{a_i} cost(k+1,i)=cost(k+1,i1)+ibefai ,其中 b e f x bef_x befx 表示 x x x 上次出现的位置。

因此可以采用线段树维护 d p k , j − 1 + c o s t ( k + 1 , i ) dp_{k,j-1} +cost(k+1,i) dpk,j1+cost(k+1,i) ,此时动规的循环为 j j j 循环在外, i i i 循环在内。每当 i i i 增大时,将 [ 1 , b e f a i − 1 ] [1,bef_{a_i}-1] [1,befai1] 范围内的点增加 i − b e f a i i-bef_{a_i} ibefai 即可维护 d p k , j − 1 + c o s t ( k + 1 , i ) dp_{k,j-1} +cost(k+1,i) dpk,j1+cost(k+1,i)

参考代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,i,j,k,l,o,p,dp[35010][105],a[35010],b[35010];
struct node
{
	ll l,r,sum,inc;
}tr[35010<<2];
void build(ll i,ll l,ll r)
{
	tr[i].l=l;tr[i].r=r;tr[i].inc=0;
	if (l==r)
	 {
	 	tr[i].sum=dp[l][j-1];
	 	return;
	 }
	ll mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	tr[i].sum=min(tr[i<<1].sum,tr[i<<1|1].sum);
}
ll query(ll i,ll l,ll r)
{
	if (tr[i].l==l && tr[i].r==r)
	{
		return tr[i].sum;
	}
	if (tr[i].inc)
	{
		tr[i<<1].inc+=tr[i].inc;
		tr[i<<1].sum+=tr[i].inc;
		tr[i<<1|1].inc+=tr[i].inc;		
		tr[i<<1|1].sum+=tr[i].inc;
		tr[i].inc=0;
	}
	ll mid=(tr[i].l+tr[i].r)>>1;
	if (r<=mid)
		return query(i<<1,l,r);
	else if (l>mid)
		return query(i<<1|1,l,r);
	else
		return min(query(i<<1,l,mid),query(i<<1|1,mid+1,r));
}
void add(ll i,ll l,ll r,ll k)
{
	if (tr[i].l==l && tr[i].r==r)
	{
		tr[i].inc+=k;
		tr[i].sum+=k;
		return;
	}
	if (tr[i].inc)
	{
		tr[i<<1].inc+=tr[i].inc;
		tr[i<<1].sum+=tr[i].inc;
		tr[i<<1|1].inc+=tr[i].inc;		
		tr[i<<1|1].sum+=tr[i].inc;
		tr[i].inc=0;
	}
	ll mid=(tr[i].l+tr[i].r)>>1;
	if (r<=mid)
		add(i<<1,l,r,k);
	else if (l>mid)
		add(i<<1|1,l,r,k);
	else
	{
	 	add(i<<1,l,mid,k);
	 	add(i<<1|1,mid+1,r,k);
	}
	tr[i].sum=min(tr[i<<1].sum,tr[i<<1|1].sum);
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for (i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	for (i=1;i<=n;i++)
		dp[i][1]=0;
	for (i=1;i<=n;i++)
	{
		dp[i][1]=dp[i-1][1];
		if (b[a[i]]!=0)
			dp[i][1]+=i-b[a[i]];
		b[a[i]]=i;
	}
	for (j=2;j<=m;j++)
	{
		build(1,1,n);
		for (i=1;i<=n;i++)
		{
			if(b[a[i]]<i&&b[a[i]]>1)
				add(1,1,b[a[i]]-1,i-b[a[i]]);
			b[a[i]]=i;
			if (i>=j)
				dp[i][j]=query(1,1,i-1);
			else
				dp[i][j]=1ll<<60;
		}
	}
	printf("%lld\n",dp[n][m]);
}
  • 13
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值