2025.2.8 寒假综合训练赛2题解

A. 博弈

Link:P1290 欧几里德的游戏

博弈类的题目,首先考虑找找有什么性质,从而找到“必胜态”和“必败态”。

其中,面对“必胜态”不一定取胜(看个人操作的好坏),但面对“必败态”一定输(对手不会给你做选择的机会)。

上面两条是明确且标准的定义。我们发现如果你能让对手面对“必败态”,或你可以通过一系列方式(例如通过细化操作控制先后手)使自己面对必胜态,则在双方最优操作的情况下(一般题目都这样要求,不然没法做)你自己是一定能赢的。

同时,根据定义,“必胜态”下一步的所有可能性一定是“必败态”。

以上其实就是简单的一个 sg 函数推导;由“必胜态”一步转化到“必败态”其实就是一个“纳什平衡”点。有兴趣可以自行了解。


下面带入到题目当中。设表示当前局面的状态为 ( x , y ) (x,y) (x,y),我们规定 x ≥ y x \ge y xy

发现题目要求的其实就是求 gcd 的“辗转相除”过程,只不过一次可以减多个 y y y

一个显然的必胜态:若 y ∣ x y \mid x yxx%y==0),显然先手必胜。

然后考虑 x , y x,y x,y 的大小关系:

  1. y ≤ x ≤ 2 y y \le x \le 2y yx2y:此时只能减去一次 y y y,之后先后手互换操作 ( x − y , y ) (x-y,y) (xy,y)。无法确定谁获胜。
  2. x ≥ 2 y x \ge 2y x2y:先手可以减去尽可能多的 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

考虑分情况讨论:

  1. 我们能取出 k k k x x x,那么显然这 k k k 个袋子里装的都是 x x x,我们留给自己的答案应该是 k 2 ⋅ x \frac{k}{2} \cdot x 2kx
  2. 我们怎么取都取不出 k 2 \frac{k}{2} 2k x x x,那么显然直接无解,跳过这个情况。
  3. 我们可以取出一部分完整的 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 (ik<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} 1ijnmaxfi,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 n18,也很小,所以就考虑状压 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][2n1] 方案的 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,贪心配合模拟退火等),有兴趣请自行移步洛谷题解区~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值