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=0if(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; // 统计能拆出来多少个完整的xpriority_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-totcnt += 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,贪心配合模拟退火等),有兴趣请自行移步洛谷题解区~