Codeforces Round #721(Div.2)题解(1527A~E)
Codeforces Round #721(Div.2)题解(1527A~E)
Prepared by Wogua_boy
整完OS期中考终于把这场补完了,少有的能让我全部补完的场..
A.And Then There Were K
题意:
给出一个数\(n\),找到最大的\(k\),使得\(n\&(n-1)\&(n-2)\&(n-3)\&...\&k=0\)。
\(n \leq 10^9\)
题解:
二进制运算。
考虑\(n\)的二进制形式:\(100...01101\),随便写的一个,可以发现必须要把第一位二进制位\(\&\)掉。
那么就贪心的取\(k\)为:\(011111......1\),位数和\(n\)相同。在\(\&\)的过程中剩下的所有二进制位也必然会被\(\&\)掉。
时间复杂度\(O(logn)\)。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while (c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
const int maxn=2e5+100;
int t,n,a[maxn];
int main () {
t=read();
while (t--) {
n=read();
int cnt=0;
while (n) {
cnt++;
n/=2;
}
printf("%d\n",(1<<(cnt-1))-1);
}
}
B1.Palindrome Game (easy)
题意:
给出一个01字符串,Alice和Bob在字符串上博弈,\(Alice\)先手。
每个人可以做两种操作:
(1)将一个本来为0的位改成1,花费1。
(2)翻转整个字符串,花费0。但前提是这个串不能是回文串,以及,上一次操作不能是(2)。
在easy版本中,保证字符串一开始是回文的。
Alice和Bob都足够聪明,谁会取得胜利?
题解:
博弈+分类讨论。
首先,每个人的贪心策略肯定是让字符串以回文的状态进入下一轮。
考虑在0000上博弈:
0000
1000 Alice 1
0001 Bob 0
1001 Alice 1
1101 Bob 1
1011 Alice 0
1111 Bob 1
这样两者都尽可能的贪心,但是发现打平了。
事实上后手可以通过这样一个trick来获得胜利:
0000
1000 Alice 1
1001 Bob 1
1101 Alice 1
1011 Bob 0
1111 Alice 1
大致意思就是,每当对手企图破坏回文状态,就在对手操作的回文位置补一位,如果对手只差一步就构造出回文串,就翻转字符串。
这样后手可以稳定比先手少花费2获得胜利。
当字符串为奇数长度,同时中间位置的字符是0的时候,Alice可以通过修改回文中心位置来转换先后手从而取得胜利。
当全为1的时候平局。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while (c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
const int maxn=2e5+100;
//单点修改
//翻转,在不是回文同时上一次操作不是翻转的前提下
int t,n;
string s;
int main () {
t=read();
while (t--) {
n=read();
cin>>s;
int f=0;
for (int i=0;i<s.size();i++) if (s[i]=='0') f++;
if (!f) {
printf("DRAW\n");
continue;
}
if (n%2==1&&s[n/2]=='0'&&f!=1) {
printf("ALICE\n");
continue;
}
printf("BOB\n");
}
}
B2.Palindrome game (hard)
题意:
在B1的基础上,给出的字符串任意。
题解:
博弈+分类讨论。
考虑走到回文状态的先后手。
首先Alice先手,一开始不是回文的情况下,Alice可以一直翻转字符串,强制Bob去修改,当Bob只差一步构造出回文串的时候,Alice补那一下,强制Bob下一次继续修改,Alice从而在回文串状态下获得后手。
考虑奇数长度下中心位置为0的情况,可以发现对答案没有影响。
这种特殊情况:只有两个0,一个在中心,应该输出平局。没有操作空间。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while (c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
const int maxn=2e5+100;
//单点修改
//翻转,在不是回文同时上一次操作不是翻转的前提下
int t,n;
string s;
int main () {
t=read();
while (t--) {
n=read();
cin>>s;
string s1=s;
reverse(s1.begin(),s1.end());
int f=0;
for (int i=0;i<s.size();i++) if (s[i]=='0') f++;
if (!f) {
printf("DRAW\n");
continue;
}
if (s1==s) {
if (!f) {
printf("DRAW\n");
continue;
}
if (n%2==1&&s[n/2]=='0'&&f!=1) {
printf("ALICE\n");
continue;
}
printf("BOB\n");
continue;
}
f=0;
int cnt=0;
for (int i=1;i<=n/2;i++) {
if (s[i-1]!=s[n-i]) {
f++;
}
if (s[i-1]==s[n-i]&&s[i-1]=='0') {
cnt++;
}
}
if (f==1) {
if (n%2==1&&s[n/2]=='0') {
if (cnt==0)
printf("DRAW\n");
else
printf("ALICE\n");
}
else {
printf("ALICE\n");
}
}
else
printf("ALICE\n");
}
}
C.Sequence Pair Weight
题意:
定义一个序列的重量为\((i,j)i<j\)的组数使得\(a_i=a_j\)。
给出一个序列a,询问它所有子串的重量之和。
题解:
考虑一组\(a_i=a_j\)会影响多少个子序列,显然是\(i(n-j+1)\)个子串。
那么就枚举一遍,每个位置\(i\)对答案的贡献就是前面所有和\(a_i\)相等的位置的下标之和乘上\(n-i+1\)。
随便找个数据结构维护一下就行。
时间复杂度\(O(nlogn)\)。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while (c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
const int maxn=2e5+100;
int t,n;
int a[maxn];
int main () {
t=read();
while (t--) {
n=read();
for (int i=1;i<=n;i++) a[i]=read();
map<int,long long> mp;
long long ans=0;
for (int i=1;i<=n;i++) {
ans+=1ll*mp[a[i]]*(n-i+1);
mp[a[i]]+=i;
}
printf("%lld\n",ans);
}
}
D.MEX Tree
题意:
给出一棵树,下标0到n-1。
对从0到n的每个k,求出有多少条路径MEX=k。
题解:
树上分类讨论+容斥
首先容斥计算0的答案,有多少条路径不包含0?
答案是取出0的所有相邻节点,他们内部的子树的路径数量加起来。
然后容斥计算1的答案,有多少条路径包含0不包含1?
所有经过0的路径减去经过01的路径,经过01的路径数量就是先确定01的位置,然后求出0相对于1的子树大小x和1相对于0的子树大小y,xy就是答案。这里x相对于y的子树或y相对于x的子树在下文统称相对子树。
然后从2开始计算答案。
假设当前计算的点是x。
我们要求有多少条路径包含0到x-1,却不包含x。
用A,B表示包含0到x-1的路径的最短路径的起点和终点。
如果x在A或B的相对子树内,直接将A或B的相对子树大小减去i的子树大小,根据01路径的计算方法求一遍即可,然后把A或B更新为i即可。
如果i在A到B的路径上,那么i的答案注定是0。因为要构造包含0到i-1的路径,必定会经过i。
如果i既不在A或B的相对子树内,也不在A到B的路径上,当前的答案就是A的相对子树大小乘上B的相对子树,同时后面所有路径都无法构造。
时间复杂度\(O(nlogn)\)。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while (c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
const int maxn=2e5+100;
int t,n;
int sz[maxn];
int dfn[maxn];
int tot=0;
vector<int> g[maxn];
int father[30][maxn];
int h[maxn];
void dfs (int x,int pre) {
sz[x]=1;
dfn[x]=++tot;
for (int y:g[x]) {
if (y==pre) continue;
h[y]=h[x]+1;
father[0][y]=x;
dfs(y,x);
sz[x]+=sz[y];
}
}
int lca (int x,int y) {
if (h[x]<h[y]) swap(x,y);
for (int i=20;i>=0;i--) {
if (h[x]-h[y]>>i) x=father[i][x];
}
if (x==y) return x;
for (int i=20;i>=0;i--) {
if (father[i][x]!=father[i][y]) {
x=father[i][x];
y=father[i][y];
}
}
return father[0][x];
}
int main () {
t=read();
while (t--) {
n=read();
tot=0;
for (int i=0;i<=20;i++) for (int j=0;j<n;j++) father[i][j]=0;
for (int i=0;i<n;i++) g[i].clear(),h[i]=0;
for (int i=1;i<n;i++) {
int x=read();
int y=read();
g[x].push_back(y);
g[y].push_back(x);
}
dfs(0,-1);
for (int i=1;i<=20;i++) for (int j=0;j<n;j++) father[i][j]=father[i-1][father[i-1][j]];
//容斥计算0的答案
//就是0的所有子树内部算一下路径即可
long long ans=0;
for (int y:g[0]) {
ans+=1ll*sz[y]*(sz[y]-1)/2;
}
printf("%lld ",ans);
//容斥计算1的答案
//就是所有经过0的路径减去经过01的路径
//1子树内的点不考虑即可
ans=0;
long long sum=1;
int fa=-1;
for (int y:g[0]) {
int x=sz[y];
if (dfn[1]>=dfn[y]&&dfn[1]<=dfn[y]+sz[y]-1) {
//如果1在y的子树内
fa=y;
x-=sz[1];
}
ans+=1ll*x*sum;
sum+=x;
}
printf("%lld ",ans);
int A=0,B=1;//AB分别表示之前路径的起点和终点
//fa表示如果A为0,B在A的哪个子树
for (int i=2;i<=n;i++) {
//计算mex=i的答案
//如果i==n
if (i==n) {
printf("1 ");
break;
}
//先讨论i的位置
//i在B的子树内
if (dfn[i]>=dfn[B]&&dfn[i]<=dfn[B]+sz[B]-1) {
ans=0;
long long x,y;
if (A==0) {
x=n-sz[fa];
}
else {
x=sz[A];
}
y=sz[B]-sz[i];
ans=1ll*x*y;
printf("%lld ",ans);
B=i;
}
else {
//如果i在A的子树内
if (A==0&&(dfn[i]<dfn[fa]||dfn[i]>dfn[fa]+sz[fa]-1)) {
ans=0;
long long x,y;
x=n-sz[fa]-sz[i];
y=sz[B];
ans=1ll*x*y;
printf("%lld ",ans);
A=i;
}
else if (A!=0&&dfn[i]>=dfn[A]&&dfn[i]<=dfn[A]+sz[A]-1) {
ans=0;
long long x,y;
x=sz[A]-sz[i];
y=sz[B];
ans=1ll*x*y;
printf("%lld ",ans);
A=i;
}
//如果i在A到B的路径上
//答案是0
else if (lca(A,i)==i||lca(B,i)==i) {
ans=0;
printf("%lld ",ans);
}
//如果不在
//答案是包含A到B的路径的路径数量
//
else {
long long x,y;
ans=0;
if (A==0) x=n-sz[fa];
else x=sz[A];
y=sz[B];
ans=1ll*x*y;
printf("%lld ",ans);
for (int j=i+1;j<=n;j++) printf("0 ");
break;
}
}
}
printf("\n");
}
}
E.Partition Game
题意:
给出一个长度为n的数组a。
一个数组t的花费是\(cost(t)=\sum_{x \in set(t)}last(x)-first(x)\)
请你把数组a划分成k段,使得每一段的花费之和最小。
\(n \leq 3e5+5e4,k \leq 100\)
题解:
这题完全看的官方题解,用线段树维护dp过程的转移。
让我们用dp解决这个问题。
考虑用\(f(i,j)\)表示前\(j\)个元素分成\(i\)段的答案。
用\(c(i,j)\)表示从下标\(i\)到\(j\)的子段花费。
那么有以下转移方程:
\(f(i,j)=min_{k=1}^{j-1}(f(i-1,k)+c(k+1,j))\)
如果暴力计算,时间复杂度\(O(n^2k)\)。
现在我们需要用数据结构来计算\(f(i-1,k)+c(k+1,j)\)这个过程。
我们开一个新数组\(b_k=k-lst_{a_k}\),\(lst_{a_k}\)表示\(a_k\)的上一次出现位置。
然后,我们用\(k\)颗线段树维护,比如我们已经维护了前缀\(j\)的答案,对前缀\(j+1\),我们只需要在\([0,lst_{a_{j+1}}-1]\)这个区间上加上\(b_{j+1}\)即可。
时间复杂度\(O(nklogn)\)
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while (c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
const int maxn=2e5+100;
int n,k;
int a[maxn];
struct node {
int l,r;
int sum;
int lazy;
}segTree[maxn*4];
int f[maxn];
void build (int i,int l,int r) {
segTree[i].l=l;
segTree[i].r=r;
segTree[i].lazy=0;
if (l==r) {
segTree[i].sum=f[l];
return;
}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
segTree[i].sum=min(segTree[i<<1].sum,segTree[i<<1|1].sum);
}
void spread (int i) {
if (segTree[i].lazy) {
segTree[i<<1].sum+=segTree[i].lazy;
segTree[i<<1|1].sum+=segTree[i].lazy;
segTree[i<<1].lazy+=segTree[i].lazy;
segTree[i<<1|1].lazy+=segTree[i].lazy;
segTree[i].lazy=0;
}
}
void up (int i,int l,int r,int v) {
if (segTree[i].l>=l&&segTree[i].r<=r) {
segTree[i].sum+=v;
segTree[i].lazy+=v;
return;
}
spread(i);
int mid=(segTree[i].l+segTree[i].r)>>1;
if (l<=mid) up(i<<1,l,r,v);
if (r>mid) up(i<<1|1,l,r,v);
segTree[i].sum=min(segTree[i<<1].sum,segTree[i<<1|1].sum);
}
int query (int i,int l,int r) {
if (segTree[i].l>=l&&segTree[i].r<=r) {
return segTree[i].sum;
}
spread(i);
int mid=(segTree[i].l+segTree[i].r)>>1;
int ans=1e9;
if (l<=mid) ans=min(ans,query(i<<1,l,r));
if (r>mid) ans=min(ans,query(i<<1|1,l,r));
return ans;
}
int lst[maxn];
int pre[maxn];
int b[maxn];
int main () {
n=read();k=read();
for (int i=1;i<=n;i++) {
a[i]=read();
lst[i]=pre[a[i]];
pre[a[i]]=i;
b[i]=i-lst[i];
}
for (int i=0;i<=n;i++) f[i]=1e9;
f[0]=0;
build(1,0,n);
for (int i=1;i<=k;i++) {
for (int j=0;j<=n;j++) f[j]=1e9;
f[0]=0;
for (int j=1;j<=n;j++) {
if (lst[j]) up(1,0,lst[j]-1,b[j]);
f[j]=query(1,0,j-1);
}
build(1,0,n);
}
printf("%d\n",f[n]);
}