视频讲解:BV1n54y1L7Nv
A. And Then There Were K
题目大意
给定整数
n
(
1
≤
n
≤
1
0
9
)
n(1 \leq n \leq 10^9)
n(1≤n≤109) ,求最大的整数
k
k
k ,使得
n
&
(
n
−
1
)
&
(
n
−
2
)
&
(
n
−
3
)
&
.
.
.
(
k
)
=
0
n\&(n-1)\&(n-2)\&(n-3)\&...(k)=0
n&(n−1)&(n−2)&(n−3)&...(k)=0
题解
一般遇到这种题,可以考虑打表找规律,然后发现小于
n
n
n 的最大的
2
m
−
1
2^m-1
2m−1 即是答案。
具体证明如下:
- 为使得 n n n 中的二进制最高位的 1 1 1 在与运算后消除,则答案应满足 k ≤ 2 m − 1 k \leq 2^m-1 k≤2m−1
- 因为 ( 2 m ) & ( 2 m − 1 ) = 0 (2^m) \& (2^m-1)=0 (2m)&(2m−1)=0 ,因此 k = 2 m − 1 k=2^m-1 k=2m−1 满足题意要求。
参考代码
#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(1≤n≤103) 的 01 01 01 字符串,Alice和Bob双方论轮流修改,每次修改可以进行以下操作之一:
- 将一个 0 0 0 修改为 1 1 1 ,花费 1 美元;
- 反转整个字符串,花费 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 0≤i≤2n−1 且 a i = a n − i − 1 = 0 a_i=a_{n-i-1}=0 ai=an−i−1=0 的 i i i 数量;
- d i f f diff diff :表示不对称的 0 0 0 的数量,即满足 0 ≤ i ≤ n 0 \leq i \leq n 0≤i≤n 且 a i = 0 ≠ a n − i − 1 a_i = 0 \neq a_{n-i-1} ai=0=an−i−1 的 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=1−dpsame−1,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=1−dpsame,diff−1,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=1−dpsame,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 an−i−1=0 变为 a n − i − 1 = 1 a_{n-i-1}=1 an−i−1=1 ,这样Bob花费最多和Alice一样。然而对于最后一对 a i = a n − i − 1 = 0 a_i=a_{n-i-1}=0 ai=an−i−1=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(1≤n≤105) 的序列 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 1≤l≤r 且 j ≤ r ≤ n j \leq r \leq n j≤r≤n 时,子序列 a [ l , r ] a[l,r] a[l,r] 包含点对 ( i , j ) (i,j) (i,j) ,这样的子序列有 i ∗ ( n − j + 1 ) i*(n-j+1) i∗(n−j+1) 个。
因此我们可以从左到右扫描一遍序列,并统计更新每种元素各自的下标值和
∑
i
\sum{i}
∑i , 乘上
(
n
−
j
+
1
)
(n-j+1)
(n−j+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=1∑n((n−j+1)∗i<j&ai=aj∑i)
另外由于 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(2≤n≤2⋅105) 个节点的树,节点编号从 0 0 0 到 n − 1 n-1 n−1 。
对于 [ 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,k−1] 范围内的所有节点且不包含 k k k 。
直接求解的话会发现不包含 k k k 这个条件比较难解决,但是可以考虑用差分的方法解决:
- M E X MEX MEX 为 k k k 的路径数 = = = 包含 [ 0 , k − 1 ] [0, k-1] [0,k−1] 所有节点的路径数 − - − 包含 [ 0 , k ] [0, k] [0,k] 所有节点的路径数
那么只要求解出对于任意 k ( 0 ≤ k ≤ n ) k(0 \leq k \leq n) k(0≤k≤n) ,包含 [ 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] 的路径。这时有以下几种可能性:
- 2 2 2 在 1 1 1 的子树中,则缩小蓝色范围即可;
- 1 1 1 在 2 2 2 的子树中,无需修改;
- 2 2 2 在与 1 1 1 不同的以 0 0 0 的儿子为根的子树中,则缩小红色部分即可;
- 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 和 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 3 3 3 在 2 2 2 的子树中,则修改红色范围;
- 1 1 1 和 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 3 3 3 在 1 1 1 的子树中,则修改蓝色范围;
- 1 1 1 和 2 2 2 分别在不同以 0 0 0 的儿子为根的子树中, 1 1 1 在 3 3 3 的子树中,或 2 2 2 在 3 3 3 的子树中,则无需修改;
- 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)=x∈set(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(1≤n≤35000) 的数组 a a a ,将其分为 m ( 1 ≤ m ≤ m i n ( n , 100 ) ) m(1 \leq m \leq min(n,100)) m(1≤m≤min(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,j−1+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,i−1) 和
c
o
s
t
(
k
+
1
,
i
)
cost(k+1,i)
cost(k+1,i) ,可能有以下变化:
- 若 a [ k + 1 , i − 1 ] a[k+1,i-1] a[k+1,i−1] 范围内没有 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,i−1) ;
- 若 a [ k + 1 , i − 1 ] a[k+1,i-1] a[k+1,i−1] 范围内存在 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,i−1)+i−befai ,其中 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,j−1+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,befai−1] 范围内的点增加 i − b e f a i i-bef_{a_i} i−befai 即可维护 d p k , j − 1 + c o s t ( k + 1 , i ) dp_{k,j-1} +cost(k+1,i) dpk,j−1+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]);
}