【解题报告】博弈专场 (CF 2000+)前五题
- 博弈专场 (CF 2000+)十题
这里面假设每个人都是聪明的,不会犯错,希望自己的收益最大。
A:Fox and Card Game | CF388C
题意
- Fox and Card Game | CF388C
有 n n n 堆牌,第 i i i 堆有 a i a_i ai 张牌,每堆牌从上往下,给出每张牌的分数
两个人轮流拿牌, A l i c e Alice Alice 选一堆牌,拿走这堆牌的最上面那张牌; B o b Bob Bob 则是选一堆,拿走这堆牌的最下面那张牌。 - 选完后,计算每个人的分数和,大的获胜。求每个人的最终分数。
∑ a i ≤ 1 0 4 \sum a_i\le 10^4 ∑ai≤104
思路
- 一开始感觉是无从下手的,选择决策有很多。
如果靠近牌顶有收益很大的,肯定是 A l i c e Alice Alice 能拿到的。但是她可以不一开始就去拿。当 B o b Bob Bob 拿了这堆的底的时候,她再来拿这堆的顶即可,有点像围棋的下法和思路。 - 这么一看,每堆牌就是一个独立的游戏。那么我们只考虑一堆牌。如果是偶数牌, A l i c e Alice Alice 拿走前一半, B o b Bob Bob 拿走后一半。如果是奇数,那么中间那张牌会被先手拿走。
- 如果考虑多堆奇数牌,先手拿走中间的某张牌后,先手就会变成后手,后手会拿走另一堆中间的某张牌。所以我们把奇数牌堆的中间那张牌的收益降序,然后双方轮流拿即可。
代码
- 时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x << " ] , ";show(args...);}
typedef long long ll;
const int MAX = 5e5+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
vector<int>V;
int main()
{
int n;cin >> n;
ll sumA = 0,sum = 0;
ll mx = 0;
for(int i = 1;i <= n;++i){
int s;cin >> s;
for(int j = 1;j <= s;++j){
ll t;cin >> t;
if(s & 1 && j == (s + 1) / 2)V.push_back(t); // 中间的拿出来排序
else if(j <= s / 2)sumA += t;
sum += t;
}
}
sort(V.begin(),V.end(),greater<ll>());
int sz = V.size();
for(int i = 0;i < sz;++i){
if(i%2==0)sumA += V[i]; // 双方轮流拿
}
cout << sumA + mx << " " << sum - (sumA + mx);
return 0;
}
B:Berzerk | CF786A
题意
- Berzerk
有 n n n 个球,编号 1 ∼ n 1\sim n 1∼n,环形排成一圈,第 n n n 个球是黑洞。
A l i c e Alice Alice 有一个前进集合 A A A, B o b Bob Bob 有一个前进集合 B B B
对于每一个开始位置 p p p,双方轮流选择他集合中的一个数,相当于他顺时针走几步。如果走到黑洞,他就赢了。 - 求对于每一个
p
p
p,对于每个人先手的情况,是必胜还是必输,或者是平局。
2 ≤ n ≤ 7000 2\le n\le 7000 2≤n≤7000
思路
- 会跑循环,貌似挺麻烦的。考虑到多起点单终点,我们倒着跑是一个不错的选择。
我们可以定义 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1] 表示在这个位置,谁先手,的必胜态怎么样。
如果下一步,都走到对方的必胜态,自己就是必败态。如果下一步,有一步走到对方的必败态,自己就是必胜态。
否则,就是平局。 - 感觉写起来挺麻烦的。倒着走,都走到必胜态,我们可以用一个
c
n
t
cnt
cnt 数组去做。
平局,我们可以使用一个 v i s vis vis 数组去做。
代码
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x << " ] , ";show(args...);}
typedef long long ll;
const int MAX = 7e3+50;
const int MOD = 1e9+7;
int n;
vector<int>V[2];
bool vis[MAX][2];
int mex[MAX][2];
int cnt[MAX][2];
void dfs(int x,int who){
if(vis[x][who])return;
vis[x][who] = 1;
int opp = who ^ 1;
for(auto it : V[opp]){
int xp = (x - it + n - 1) % n + 1; // 上一步
if(xp == 1)continue; // 上一步是黑洞,不可能
if(!mex[x][who]){ // 这一步必败,上一步必胜
mex[xp][opp] = 1;
dfs(xp,opp);
}else if(++cnt[xp][opp] == V[opp].size()){ // 上一步的下一步全是必胜,上一步必败
mex[xp][opp] = 0;
dfs(xp,opp);
}
}
}
int main()
{
cin >> n;
int s;
cin >> s;
while(s--){
int t;cin >> t;
V[0].push_back(t);
}
cin >> s;
while(s--){
int t;cin >> t;
V[1].push_back(t);
}
dfs(1,0);dfs(1,1);
for(int i = 2;i <= n;++i)
cout << (vis[i][0] ? mex[i][0] ? "Win" : "Lose" : "Loop") << " ";
puts("");
for(int i = 2;i <= n;++i)
cout << (vis[i][1] ? mex[i][1] ? "Win" : "Lose" : "Loop") << " ";
return 0;
}
C:Ithea Plays With Chtholly | CF896B
题意
- Ithea Plays With Chtholly | CF896B
有 n n n 张纸,一开始没有都是空。
有 m m m 轮,每一次给定一个 [ 1 , c ] [1,c] [1,c] 的数字。
给你数字之后,你需要选择把它写在那一张纸上,若纸上之前写过数字,则覆盖。
你需要摆完一个数字,他才给你下一个数字。
问你怎么摆,让最后 n n n 张纸上的数字非单调递减。 -
n
,
m
≥
2
n,m\ge 2
n,m≥2
1 ≤ c ≤ 1000 1\le c\le 1000 1≤c≤1000
1 ≤ n ⋅ ⌈ c 2 ⌉ ≤ m ≤ 1000 1\le n \cdot \lceil \frac{c}{2}\rceil \le m\le 1000 1≤n⋅⌈2c⌉≤m≤1000
思路
- 非常神奇的交互题。也是一开始感觉摆数字让最后非单调递减,策略比较复杂的感觉。
但是仔细想想,假设它最开始给了你个 1 1 1,你摆在最左边是毫无疑问的。
假设最开始给了你个 2 2 2,你摆在最左边好像也可以,但如果再给了你个 1 1 1,你摆成 [ 2 , 1 ] [2,1] [2,1] 就很劣,不如把 2 2 2 替换成 1 1 1 - 经过上述非常离散的思考,我们感觉一直维护一个非单调递减的序列就可以了,做法有点类似求最长上升子序列的过程。
- 但是这样
W
A
WA
WA了。再次仔细考虑,我们的次数超过了
m
m
m 轮了。假设它给你
[
1
,
c
]
[1,c]
[1,c] 范围内的数字,给你
c
c
c 的话,你放在最右位置是毫无疑问的。
于是,凭感觉,我们把值分成小值和大值,序列从左往右维护一个小值的非递减序列,从右往左维护一个大值的非递增序列。这样,次数就够了。 - 严格分析轮数的话,考虑最劣情况的维护非递增序列的次数,即给定序列为 [ x , x − 1 , x − 2 , ⋯ , 1 , x , x − 1 , ⋯ ] [x,x-1,x-2,\cdots,1,x,x-1,\cdots] [x,x−1,x−2,⋯,1,x,x−1,⋯],即可以分析得到轮数的最劣情况就是 n ⋅ ⌈ c 2 ⌉ n \cdot \lceil \frac{c}{2}\rceil n⋅⌈2c⌉
代码
- 时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x << " ] , ";show(args...);}
typedef long long ll;
const int MAX = 1e3+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
int arr[MAX];
int arr_iv[MAX];
int main()
{
int n,m,c;cin >> n >> m >> c;
fill(arr+1,arr+n+1,INF);
fill(arr_iv+1,arr_iv+n+1,INF);
while(1){
int t;cin >> t;
if(t <= c / 2){
int p = upper_bound(arr+1,arr+1+n,t) - arr;
cout << p << endl;
arr[p] = t;
arr_iv[n - p + 1] = -t;
fflush(stdout);
}else{
int p = upper_bound(arr_iv+1,arr_iv+1+n,-t) - arr_iv;
cout << n - p + 1 << endl;
arr[n - p + 1] = t;
arr_iv[p] = -t;
fflush(stdout);
}
// for(int i = 1;i <= n;++i){
// show(i,arr[i],arr_iv[i]);
// }
bool can = true;
for(int i = 1;i <= n;++i){
if(arr[i] == INF){
can = false;
break;
}
if(arr[i] < arr[i-1]){
can = false;
break;
}
}
if(can)break;
}
return 0;
}
D:Lieges of Legendre | CF603C
题意
- Lieges of Legendre | CF603C
n n n 堆石头,每堆有 a i a_i ai 个石头。
两个人轮流拿石头。每次可以:
(1) 选择一堆,拿走其中一个石头
(2)选择一堆,这堆的石头个数为 2 x 2x 2x,把它用 k k k 堆石头,每堆 x x x 个石头替换
谁不能拿就输了。求先手是否必胜。 -
1
≤
n
≤
1
0
5
1\le n\le 10^5
1≤n≤105
1 ≤ a i , k ≤ 1 0 9 1\le a_i,k\le 10^9 1≤ai,k≤109
思路
- 石头,考虑
S
G
SG
SG 值。
设 f ( x ) f(x) f(x) 表示 x x x 个石头的 S G SG SG 值。
一个可以转移到 f ( x − 1 ) f(x-1) f(x−1)
若 x x x 是偶数,可以变成 k k k 个 f ( x / 2 ) f(x/2) f(x/2) 的异或值。 - 容易想到按照 k k k 的奇偶性分类讨论。
- 如果
k
k
k 是偶数,我们能算出来
f
[
0
,
1
,
2
]
=
{
0
,
1
,
2
}
f[0,1,2]=\{0,1,2\}
f[0,1,2]={0,1,2}
如果 x x x 是奇数,那么可以得到 f ( x ) = m e x { f ( x − 1 ) } f(x)=mex\{f(x-1)\} f(x)=mex{f(x−1)}
如果 x x x 是偶数,那么可以得到 f ( x ) = m e x { f ( x − 1 ) , 0 } f(x)=mex\{f(x-1),0\} f(x)=mex{f(x−1),0},因为 k k k 个 f ( x / 2 ) f(x/2) f(x/2) 的异或值为 0 0 0
我们可以直接得到 f ( x ) = 0 f(x)=0 f(x)=0,如果 x x x 是大于 2 2 2 的奇数; f ( x ) = 1 f(x)=1 f(x)=1,如果 x x x 是大于 2 2 2 的偶数。 - 如果
k
k
k 是奇数,我们能算出来
f
[
0
,
1
,
2
,
3
,
4
]
=
{
0
,
1
,
0
,
1
,
2
}
f[0,1,2,3,4]=\{0,1,0,1,2\}
f[0,1,2,3,4]={0,1,0,1,2}
如果 x x x 是奇数,仍然 f ( x ) = m e x { f ( x − 1 ) } f(x)=mex\{f(x-1)\} f(x)=mex{f(x−1)}
如果 x x x 是偶数,那就 f ( x ) = m e x { f ( x − 1 ) , f ( x / 2 ) } f(x)=mex\{f(x-1),f(x/2)\} f(x)=mex{f(x−1),f(x/2)} 因为 k k k 个 f ( x / 2 ) f(x/2) f(x/2) 的异或值为 f ( x / 2 ) f(x/2) f(x/2)
我们能得到一个结论,如果 x x x 是大于 4 4 4 的奇数,那么 f ( x ) = 0 f(x)=0 f(x)=0。否则, f ( x ) > 0 f(x)>0 f(x)>0,我们可以归纳法去做,假设成立,然后根据 m e x { f ( x − 1 ) } mex\{f(x-1)\} mex{f(x−1)} 可以证明。
我们接下来只要递归计算 f ( x ) f(x) f(x) , x x x 是偶数的情况即可。每次都除以 2 2 2,是在 log \log log 的数量级可以求出来的。 - 当然有人说:结论怎么想得出来的?你可以打表找规律,但是记得按 k k k 的奇偶性去看。
代码
- 时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x << " ] , ";show(args...);}
typedef long long ll;
const int MAX = 1e3+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const int pre[] = {0, 1, 0, 1, 2};
int sg(int x,int k){
if(k & 1){
if(x < 5)return pre[x];
if(x & 1)return 0;
return sg(x / 2,k) == 1 ? 2 : 1;
}else{
if(x <= 2) return x == 1 ? 1 : 2;
return (x % 2) ^ 1;
}
}
int main()
{
int n,k;
scanf("%d%d",&n,&k);
int mex = 0;
for(int i = 1;i <= n;++i){
int t;scanf("%d",&t);
mex ^= sg(t,k);
}
puts(mex?"Kevin":"Nicky");
return 0;
}
E:Funny Game | CF731E
题意
- Funny Game | CF731E
有 n n n 个数,两个人轮流操作。
每一次,选择至少两个,把最左边的 x x x 个数字合并成一个新的数字,值为这 x x x 个数字的和 y y y,操作的人获得 y y y 分。
选完后,第一个人的分数减去第二个人的分数的差值为 C C C,第一个人希望让 C C C 最大,第二个人希望让 C C C 最小,你需要求出这个 C C C。 - 2 ≤ n ≤ 2 × 1 0 5 2\le n\le 2\times 10^5 2≤n≤2×105
思路
- 每次就是选择一个前缀和,设
d
p
[
i
]
[
0
/
1
]
dp[i][0/1]
dp[i][0/1] 表示已经选择好前
i
i
i 个数了,接下来由哪个人开始选。
写下状态转移方程:
d p [ i ] [ 0 ] = max { d p [ j ] [ 1 ] + p r e [ j ] } dp[i][0]=\max\{dp[j][1] + pre[j]\} dp[i][0]=max{dp[j][1]+pre[j]}
d p [ i ] [ 1 ] = min { d p [ j ] [ 0 ] + p r e [ j ] } dp[i][1]=\min\{dp[j][0] + pre[j]\} dp[i][1]=min{dp[j][0]+pre[j]}
然后考虑暴力做时间不够的,只要记录一个最大值和最小值,直接转移即可。 - 注意至少选择两个,也就是说我们的答案是 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0] 而不是 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]
- 还有别的更简单的做法。考虑每个操作的人都是希望最大化自己的分数的,所以可以写成:
ll ans = pre[n];
for(int i = n-1;i >= 2;--i)
ans = max(ans,pre[i] - ans); // -ans 是因为差值对于不同双方是取反的
cout << ans;
也是非常的妙。
代码
- 时间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x << " ] , ";show(args...);}
typedef long long ll;
const int MAX = 2e5+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
ll pre[MAX];
ll dp[MAX][2];
int main()
{
int n;
scanf("%d",&n);
for(int i = 1;i <= n;++i){
ll t;scanf("%lld",&t);
pre[i] = pre[i-1] + t;
}
ll mx = pre[n];
ll mn = -pre[n];
for(int i = n - 1;i >= 0;--i){
dp[i][0] = mx;
dp[i][1] = mn;
if(dp[i][0] - pre[i] < mn){
mn = dp[i][0] - pre[i];
}
if(dp[i][1] + pre[i] > mx){
mx = dp[i][1] + pre[i];
}
// show(i,dp[i][0],dp[i][1]);
}
printf("%lld",dp[1][0]);
return 0;
}