Codeforces Round 901 (Div. 2) - A B C D(简单DP) E(状态压缩 + bfs + 位运算) F(概率DP)

题目传送门
E 题好题,值得品味~
F 题概率 DP

A. Jellyfish and Undertale

贪心考虑每次时间减到 1 时能否使用工具加时间,注意总时间上限是 a

B. Jellyfish and Game

贪心考虑每位选手肯定都是用自己的最小换对手的最大
如果说总操作次数为奇数次,那么先手可以先操作一次,然后跟着对手操作以维护自己的最大
如果说总操作次数为偶数次,那么先手操作一次,后手操作一次后,后手为了维护自己的最大跟着先手操作,进入循环

ll n, m, k, a[maxm], b[maxm];

void solve(){
	cin >> n >> m >> k;
	for(int i = 0; i < n; ++ i) cin >> a[i];
	for(int i = 0; i < m; ++ i) cin >> b[i];
	sort(a, a + n);
	sort(b, b + m);
	if(a[0] < b[m - 1]) swap(a[0], b[m - 1]);
	if(k % 2 == 0){
		sort(a, a + n);
		sort(b, b + m);
		if(b[0] < a[n - 1]) swap(b[0], a[n - 1]);
	}
	ll ans = accumulate(a, a + n, 0ll);
	cout << ans << '\n';
	return ;
}

C. Jellyfish and Green Apple

首先,如果给定的苹果个数 $n \ge m $,那么可以给 m 个人每人相同个数的苹果将问题转化为 $n < m $ 的情况
如果苹果刚好够分,那么不用切,输出 0 即可
反之,对于 n 个苹果 m 个人,划分的最少总苹果片个数为 $a = n * m / \gcd(n, m) $,每个人分到的苹果片个数为 $b = a / m $,一个苹果需要被划分为 $c = a / n $ 片
如果说 c 不是 2 的幂次方,那么无法通过对半分割来实现划分,输出 -1
反之,现在还需要考虑的问题就是对半划分的最少次数。将我们已经划分出来的每人分到的片数采取两两合并的方法保留大片即可,即二进制划分 b
如果说 b 的二进制表示中 1 的个数为 t 个,那么最终需要的总的切刀数即为 $t * m - n $,因为每切一刀,苹果总个数多一块

void solve(){
	ll n, m;
	cin >> n >> m;
	n %= m;
	if(n == 0){// 刚好可以不切完全分完
		cout << "0\n"; return ;
	}
	// a为划分的总片数,b为每个人分到的片数,c为一个苹果需要被切成多少片
	ll a = n * m / __gcd(n, m), b = a / m, c = a / n;
	if(c & (c - 1)){// 无法通过对半分苹果取得
		cout << "-1\n"; return ;
	}
	ll ans = 0;
	while(b){// 计算 b 二进制表示中1的个数
		if(b & 1) ++ ans;
		b >>= 1;
	}
	ans = ans * m - n;
	cout << ans << '\n';
	return ;
}

D. Jellyfish and Mex

首要的目标就是找到一种最节约的方式使得 $mex(a) = 0 $,因为此时其对答案的贡献为 0
显然得考虑 DP 求解
观察题目的范围,可以 $O(n^2) $ 求解
状态
定义 \(dp [i]\) 表示 $mex(a) = i $ 时所需要的最小花费

转移
先统计每个数的出现次数 $c[i] $,因为 $n \le 5000 $ ,故仅统计 5000 以下的数即可
设初始的时候 \(mex(a) = m , dp[m] = 0\)
可以发现一点就是,如果将所有数 \(i (i < m)\) 移除时 时,$mex(a) = i $,而且连续移除当前数 i 更加划算
故 $dp_i $ 可由 \(dp_j (j > i)\) 转移而来,$dp_i = min(dp[j] + (c[i] - 1) * j + i), j \in [i + 1, m] $
最终的答案即为 $dp[0] $

void solve(){
	int n, t;
	cin >> n;
	vector<int> c(maxm, 0), dp(maxm, inf);
	for(int i = 0; i < n; ++ i){
		cin >> t;
		if(t <= n) ++ c[t];
	}
	t = 0;
	while(c[t]) ++ t;
	dp[t] = 0;
	for(int i = t - 1; i >= 0; -- i){
		for(int j = t; j > i; -- j){
			dp[i] = min(dp[i], dp[j] + (c[i] - 1) * j + i);
		}
	}
	cout << dp[0] << '\n';
	return ;
}

E. Jellyfish and Math

难,参考官方题解

题目给定了 4 种位运算操作,很容易想到拆位考虑
对于每一位,由 \((a, b, m)\) 转变为 \((c, d)\),可以发现 \((a, b, m)\) 一共有 8 种情况,\((c, d)\) 一共有 4 种情况

题目中的给定的 30 个位中会包含几种 \((a, b, m)\),要使得转移能够成立,那么转移不同位上相同的 \((a_i, b_i, m_i )\) 一定是对应相同的 \((c_j, d_j)\),如果对应不相同,而题目的操作又是对所有的位一起操作的,则此情况无解,输出-1

由此,可以发现,我们可以预处理出所有的转移,这样查询的时候考虑 30 位的限制即可
那么简单,利用 8 个数表示状态,再进行转移即可,但是这样没法考虑 30 位的限制。那我们将 8 种状态一起表示出来,利用一个 8 位的四进制数即可,再加上可能原输入的数对于某一种 \((a_i, b_i, m_i )\) 的转移没有限制,那么需要多考虑一种情况,故利用一个 8 位的五进制数即可表示所有的转移可能

五进制数 mask 的第 i 位表示 \((a_i, b_i, m_i)\),分别为111,110,011,010,001,000;其上的值表示 \((c_j, d_j)\),100,11,10,01,00 (100表示无限制)
最小的操作次数可以利用 bfs 求解。初始五进制数为 mask = 33221100(五进制数),此时 $dp[mask] = 0 $,即 $(a_i, b_i, m_i) = (a_i, b_i) $
这样我们遍历四种操作方式,记录最小值即可
遍历完之后需要再考虑每一个位上为 4 即无限制的情况,当前状态的值显然是当前位上任取的最小值

对于每一次询问,我们定义初始 mask = 44444444(五进制),对于 30 位的每一位,均考虑其限制并修改 mask 相应位为相应的值,最终判断其是否可达,输出答案即可

具体细节见代码,函数的作用在代码中标注了,唯一的关键就是记住五进制表示和状态压缩的表示!!!

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int S = 4e5 + 5, inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;
int pw5[10] = {}, dp[S] = {};
queue<int> Q;

void checkmin(int &x, int y){
	if(y < x)  x = y; return ;
}

int w(int mask, int i){
	return (mask / pw5[i]) % 5;// 返回 mask 在第 i 位的值 (五进制的值)
}

int f(int a, int b, int m){// (a, b, m) 转为 十进制表示, 共 8 种情况
	return (a << 2) | (b << 1) | m;
}

int g(int c, int d){// (c, d) 转为 十进制表示,共 4 种情况
	return (c << 1) | d;
}

int work(int mask, int opt){
	// 执行第 opt 步操作
	int ret = 0;
	for(int a = 0; a < 2; ++ a){
		for(int b = 0; b < 2; ++ b){
			for(int m = 0; m < 2; ++ m){// 对 8 种情况分别判断结果
				int x = w(mask, f(a, b, m)), c = x >> 1, d = x & 1;
				if(opt == 1) c = c & d;
				else if(opt == 2) c = c | d;
				else if(opt == 3) d = c ^ d;
				else d = m ^ d;
				ret += pw5[f(a, b, m)] * g(c, d);
			}
		}
	}
	return ret;
}

void pre(){
	pw5[0] = 1;// pw[i] 表示 5 的 i 次方
	for(int i = 1; i <= 8; ++ i) pw5[i] = 5 * pw5[i - 1];// 预处理 5 的 i 次方
	mem(dp, inf);
	int mask = 0;
	// 计算初始的 mask,利用 8 位五进制数表示状态
	/* 五进制的 mask 每一位均表示
	   初始的 mask 是什么?五进制表示为33221100(5进制),即为 8 种 (a, b, m) 的初始态
	*/
	for(int a = 0; a < 2; ++ a){
		for(int b = 0; b < 2; ++ b){
			for(int m = 0; m < 2; ++ m){
				mask += pw5[f(a, b, m)] * g(a, b);
			}
		}
	}
	dp[mask] = 0; // 初始态 0 步可达
	Q.push(mask);
	while(Q.size()){// bfs
		int s = Q.front();
		Q.pop();
		for(int opt = 0; opt < 4; ++ opt){// 遍历四种操作
			int t = work(s, opt);
			if(dp[t] == inf){// 操作后的数第一次抵达
				dp[t] = dp[s] + 1; Q.push(t);
			}
		}
	}
	for(mask = 0; mask < pw5[8]; ++ mask){
		for(int i = 0; i < 8; ++ i){
			if(w(mask, i) == 4){// val == 4 表示可以取所有情况,故可以取当前五进制位上的最小值
				for(int x = 1; x <= 4; ++ x){
					checkmin(dp[mask], dp[mask - x * pw5[i]]);
				}
				break;
			}
		}
	}
	return ;
}

void solve(){
	int A, B, C, D, M, mask = pw5[8] - 1;// mask 五进制初始为 44444444
	cin >> A >> B >> C >> D >> M;
	for(int i = 0; i < 30; ++ i){// 查看所有位的限制
		int a = (A >> i) & 1, b = (B >> i) & 1,
			c = (C >> i) & 1, d = (D >> i) & 1, m = (M >> i) & 1;
		if(w(mask, f(a, b, m)) == 4)// 这种情况未被占用
			mask -= (4 - g(c, d)) * pw5[f(a, b, m)];
		else if(w(mask, f(a, b, m)) != g(c, d)){// 出现冲突,无解
			cout << "-1\n"; return ;
		}
	}
	cout << (dp[mask] < inf ? dp[mask] : -1) << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	pre();
	int _ = 1;
	cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

F. Jellyfish and EVA

目的就是求赢的最大概率值
在城市之间移动遵循两条规则:

  • 选成功概率最大的一条路
  • 随机的选择一条路

还有就是如果说两个人的选择不同,那么选过的两条路都会无法选择,即对于所在的城市来说选择将会少 2 种

那么,我们是否可以求出每条边的成功选择概率?因为有附加条件的存在
很容易联想到 DP 求解这个概率

状态:
\(p[i][j]\) 表示选择当前城市 i 条边中成功概率由大到小排名第 j 的边的选择概率

转移:
初始均为 0, \(p[1][1] = 1; p[2][1] = 0.5; p[i][1] = 1 / i\)
那么排名大于一的边想要被成功选择,只能通过附加条件炸路使其变为排名第一的路
炸路存在两种情况:
一是选择了第一条边和另一条排名大于 j 的边,那么当前边的排名上升 1
\(p[i][j] += 1.0 * (i - j) / i * p[i - 2][j - 1]\)
二是选择了第一条边和另一条排名大于一小于 j 的边,那么当前边的排名上升 2
\(p[i][j] += 1.0 * (j - 2) / i * p[i - 2][j - 2]\)

至此,预处理部分完成
由于原路都只能从前往后走,所以 $dp[n] = 1 $,之后从后往前遍历城市,每座城市的成功概率 = 所有 (出发可达城市的选择概率 乘上 该城市的成功概率) 的和

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int maxm = 5e3 + 5, inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;
double p[maxm][maxm];

void pre(){
	// p[i][j] 表示当前节点的第 i 条边里面一起选择第 j 条边的概率
	p[1][1] = 1;
	p[2][1] = 0.5;
	for(int i = 3; i < maxm; ++ i){
		p[i][1] = 1.0 / i;
		for(int j = 2; j <= i; ++ j){
			p[i][j] += 1.0 * (i - j) / i * p[i - 2][j - 1];// 选择第一条和 j 后面的
			p[i][j] += 1.0 * (j - 2) / i * p[i - 2][j - 2];// 选择第一条和 j 前面的
		}
	}
	return ;
}

void solve(){
	int n, m;
	cin >> n >> m;
	vector<int> e[n + 1];
	vector<double> dp(n + 1, 0);
	for(int i = 0; i < m; ++ i){
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
	}
	dp[n] = 1.0;
	for(int i = n; i > 0; -- i){
		sort(e[i].begin(), e[i].end(), [&](int u, int v){
			return dp[u] > dp[v];
		});
		int size = e[i].size();
		for(int j = 0; j < size; ++ j){
			dp[i] += dp[e[i][j]] * p[size][j + 1];
		}
	}
	cout << fixed << setprecision(15) << dp[1] << '\n';
	return ;
}

signed main(){
	pre();
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}
posted @ 2023-10-04 21:34  Qiansui  阅读(185)  评论(1)    收藏  举报