A. 博弈
Link:P1290 欧几里德的游戏
博弈类的题目,首先考虑找找有什么性质,从而找到“必胜态”和“必败态”。
其中,面对“必胜态”不一定取胜(看个人操作的好坏),但面对“必败态”一定输(对手不会给你做选择的机会)。
上面两条是明确且标准的定义。我们发现如果你能让对手面对“必败态”,或你可以通过一系列方式(例如通过细化操作控制先后手)使自己面对必胜态,则在双方最优操作的情况下(一般题目都这样要求,不然没法做)你自己是一定能赢的。
同时,根据定义,“必胜态”下一步的所有可能性一定是“必败态”。
以上其实就是简单的一个 sg 函数推导;由“必胜态”一步转化到“必败态”其实就是一个“纳什平衡”点。有兴趣可以自行了解。
下面带入到题目当中。设表示当前局面的状态为 ( x , y ) (x,y) (x,y),我们规定 x ≥ y x \ge y x≥y。
发现题目要求的其实就是求 gcd 的“辗转相除”过程,只不过一次可以减多个 y y y。
一个显然的必胜态:若
y
∣
x
y \mid x
y∣x(x%y==0
),显然先手必胜。
然后考虑 x , y x,y x,y 的大小关系:
- y ≤ x ≤ 2 y y \le x \le 2y y≤x≤2y:此时只能减去一次 y y y,之后先后手互换操作 ( x − y , y ) (x-y,y) (x−y,y)。无法确定谁获胜。
- x ≥ 2 y x \ge 2y x≥2y:先手可以减去尽可能多的 y y y,使局面变成 ( x m o d y , y ) (x\ mod\ y, y) (x mod y,y)。也可以少减一个 y y y,让对手操作一次变为 ( x m o d y , y ) (x\ mod\ y, y) (x mod y,y)。无论 ( x m o d y , y ) (x\ mod\ y, y) (x mod y,y) 是先手获胜还是后手获胜,当前操作者都可以改变先后手(即“少减一个y”)使自己获胜。所以此时是必胜态。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 如果Stan获胜返回1,否则返回0
int dfs(int x, int y, int now){ // 如果当前该Stan,now=1,否则now=0
if(x < y) swap(x, y); // 保证x总比y大
if(x % y == 0){
return now == 1; // 可以直接赢
}
if(y <= x && x <= 2 * y){
return dfs(x - y, y, now ^ 1); // 只能先操作一次,再看
}
else if(x >= 2 * y){
return now == 1; // 上面说过,当前操作者此时一定可以改变先后手使自己获胜
}
return -1; // 理论上不会到达这里
}
void solve()
{
int x, y; cin >> x >> y;
puts(dfs(x, y, 1)? "Stan wins" : "Ollie wins");
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
int T; cin >> T;
while(T --) solve();
return 0;
}
B. 宝石探险家
Link:P6002 (USACO20JAN) Berry Picking S
背景:这道题没有一个人赛时满分,CD题都有人过,很多人被这道题卡在了 AK 的半路上。然而老师说做法特别简单?实际上也是这样的,看来是我们太蒟了。。。
进入正题:
发现每个树上最多有 1000 1000 1000 个果子,所以我们可以考虑枚举一个答案 x x x,保证要给对手的 k 2 \frac{k}{2} 2k 个篮子里的最小值为 x x x,这也就同时限制了给自己留下的 k 2 \frac{k}{2} 2k 个篮子里的最大值为 x x x。
考虑分情况讨论:
- 我们能取出 k k k 个 x x x,那么显然这 k k k 个袋子里装的都是 x x x,我们留给自己的答案应该是 k 2 ⋅ x \frac{k}{2} \cdot x 2k⋅x。
- 我们怎么取都取不出 k 2 \frac{k}{2} 2k 个 x x x,那么显然直接无解,跳过这个情况。
- 我们可以取出一部分完整的 x x x,但不够给自己分( k 2 < ⌊ k x ⌋ < k \frac{k}{2} < \lfloor\frac{k}{x}\rfloor < k 2k<⌊xk⌋<k)。这时候我们会把 k 2 \frac{k}{2} 2k 个完整的 x x x 给对方,然后给自己尽可能多的完整的 x x x,如果不够就在剩下的余数里挑大的给自己。
第三种情况维护余数可以考虑用优先队列实现。
时间复杂度 O ( n 2 log n ) O(n^2 \log n) O(n2logn)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, k, a[1005];
void solve()
{
cin >> n >> k;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
int ans = 0;
for(int x = 1, maxx = *max_element(a + 1, a + n + 1); x <= maxx; x ++){ // 枚举中间点
int tot = 0; // 统计能拆出来多少个完整的x
priority_queue <int> Q; // 记录余数
for(int i = 1; i <= n; i ++){
tot += a[i] / x;
Q.push(a[i] % x);
}
if(tot < k / 2) continue; // 无解
if(tot >= k){
ans = max(ans, x * (k / 2)); continue; // 足够分,直接对半
}
int cnt = 0; // 记录自己能拿到的个数
cnt += (tot - k / 2) * x; // 先把完整的x算上
for(int i = 1; i <= k - tot; i ++){ // 选的个数:k/2-(tot-k/2)=k-tot
cnt += Q.top(); Q.pop();
}
ans = max(ans, cnt);
}
cout << ans << '\n';
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solve();
return 0;
}
C.星际能量站
Link:P3146 (USACO16OPEN) 248 G
看到“合并”这个套路,加上 n n n 很小,不难想到区间 dp。
设 f i , j f_{i,j} fi,j 表示融合区间 [ i , j ] [i,j] [i,j] 可以获得的最高频率。
转移考虑枚举一个断点 k ( i ≤ k < j ) k\ (i \le k < j) k (i≤k<j),若 f i , k = f k + 1 , j f_{i,k}=f_{k+1,j} fi,k=fk+1,j,则f[i][j]可以合并成一个新的值,尝试转移 f i , k + 1 f_{i,k}+1 fi,k+1;
然后, [ i , j ] [i,j] [i,j] 还可以由两个小区间的答案合并得来,尝试转移 max { f i , k , f k + 1 , j } \max\{f_{i,k},f_{k+1,j}\} max{fi,k,fk+1,j}。
但这样会有问题,我们会把 [ 2 , 6 , 2 ] [2,6,2] [2,6,2] 这样原来无法合并的区间看成了一个单独的 6 6 6!这显然是会影响答案的(看注释掉的那一行转移)
考虑修改状态的定义,
f
i
,
j
f_{i,j}
fi,j 表示如果
[
i
,
j
]
[i,j]
[i,j] 可以融合成一个数的话能得到的最高频率。
那么,对于无法合并成一个的(例如
[
2
,
6
,
2
]
[2,6,2]
[2,6,2])
f
f
f 值会是
−
∞
- \infty
−∞。
这样定义状态的话,答案应该是所有
f
i
,
j
f_{i,j}
fi,j 的最大值
max
1
≤
i
≤
j
≤
n
f
i
,
j
\max \limits_{1 \le i \le j \le n} f_{i,j}
1≤i≤j≤nmaxfi,j。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, ans = -1e9;
int a[250];
int f[250][250]; // f[i][j]: 融合区间[i,j]可以获得的最高频率
void solve()
{
memset(f, -0x3f, sizeof f);
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> a[i];
f[i][i] = a[i]; // 融合长度为1的区间,得到的只能是a[i]本身
ans = max(ans, f[i][i]);
}
for(int l = 2; l <= n; l ++){
for(int i = 1; i + l - 1 <= n; i ++){
int j = i + l - 1;
for(int k = i; k <= j - 1; k ++){
if(f[i][k] == f[k + 1][j]){
f[i][j] = max(f[i][j], f[i][k] + 1);
}
// f[i][j] = max(f[i][j], max(f[i][k], f[k + 1][j])); // 不能转移
}
ans = max(ans, f[i][j]);
}
}
cout << ans << '\n';
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solve();
return 0;
}
这道题还有一个加强版:P3147 (USACO16OPEN) 262144 P,数据更极限,用的是用倍增思想进行区间 dp 的trick,有兴趣可以了解一下。
D.量子仓库
Link:P3052 (USACO12MAR) Cows in a Skyscraper G
发现这道题有一个显然的暴力就是用全排列上手,枚举全排列,然后按照顺序每次都尽可能装接近 W W W 的物品传送。
但这样时间复杂度是指数级别的。Sheryang 说过:全排列能做的题大多都可以用状压优化复杂度。
这道题的 n ≤ 18 n \le 18 n≤18,也很小,所以就考虑状压 dp。
设 f [ i ] [ j ] f[i][j] f[i][j] 表示当前用了 i i i 组,选择状态为 j j j 时当前电梯里的最小重量。
那么答案就是最小的存在 f [ i ] [ 2 n − 1 ] f[i][2^n-1] f[i][2n−1] 方案的 i i i。
考虑转移,如果物品
k
k
k 可以放在当前这一组里,且容量满足限制
f
[
i
]
[
j
]
+
c
[
k
]
≤
W
f[i][j] + c[k] \le W
f[i][j]+c[k]≤W,就可以继续往这一组里放:
f
[
i
]
[
j
∣
(
1
<
<
k
)
]
=
m
i
n
(
f
[
i
]
[
j
∣
(
1
<
<
k
)
]
,
f
[
i
]
[
j
]
+
c
[
k
]
)
f[i][j\ | \ (1<<k)] = min(f[i][j\ | \ (1<<k)],\ f[i][j] + c[k])
f[i][j ∣ (1<<k)]=min(f[i][j ∣ (1<<k)], f[i][j]+c[k])
如果这一组不够
f
[
i
]
[
j
]
+
c
[
k
]
>
W
f[i][j] + c[k] > W
f[i][j]+c[k]>W,那么就只能新开一组:
f
[
i
+
1
]
[
j
∣
(
1
<
<
k
)
]
=
m
i
n
(
f
[
i
+
1
]
[
j
∣
(
1
<
<
k
)
]
,
c
[
k
]
)
f[i+1][j\ |\ (1<<k)] = min(f[i+1][j\ |\ (1<<k)],\ c[k])
f[i+1][j ∣ (1<<k)]=min(f[i+1][j ∣ (1<<k)], c[k])
初始化考虑初始化为
+
∞
+\infty
+∞。初状态即为任选一个物品放到第一个袋子中:
f
[
1
]
[
1
<
<
i
]
=
c
[
i
]
f[1][1<<i]=c[i]
f[1][1<<i]=c[i]。
时间复杂度为 O ( 2 n × n 2 ) O(2^n \times n^2) O(2n×n2)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, W;
int c[20], f[20][1 << 20];
void solve()
{
memset(f, 0x3f, sizeof f); // 初始化为极大值
cin >> n >> W;
for(int i = 1; i <= n; i ++){
cin >> c[i];
}
for(int i = 1; i <= n; i ++){ // 初始化只选一个的情况
f[1][1 << i-1] = c[i];
}
for(int i = 0; i <= n; i ++){ // 枚举组数
for(int j = 0; j < (1 << n); j ++){ // 枚举已经选了的状态
if(f[i][j] == f[0][0]) continue; // 注意判断状态f[i][j]是否存在
for(int k = 1; k <= n; k ++){ // 枚举下一个选哪个位置
if(j & (1 << k-1)) continue; // 保证k之前没有选过
if(f[i][j] + c[k] <= W){
f[i][j | (1 << k-1)] = min(f[i][j | (1 << k-1)], f[i][j] + c[k]);
}
else{
f[i + 1][j | (1 << k-1)] = min(f[i + 1][j | (1 << k-1)], c[k]);
}
}
}
}
for(int i = 0; i <= n; i ++){
if(f[i][(1 << n) - 1] != f[0][0]){
cout << i << '\n';
return ;
}
}
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solve();
return 0;
}
当然,这道题还有一些神秘做法(比如裸搜剪枝、迭代加深搜索IDDFS,贪心配合模拟退火等),有兴趣请自行移步洛谷题解区~