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
首先,如果给定的苹果个数
如果苹果刚好够分,那么不用切,输出 0 即可
反之,对于 n 个苹果 m 个人,划分的最少总苹果片个数为
如果说 c 不是 2 的幂次方,那么无法通过对半分割来实现划分,输出 -1
反之,现在还需要考虑的问题就是对半划分的最少次数。将我们已经划分出来的每人分到的片数采取两两合并的方法保留大片即可,即二进制划分 b
如果说 b 的二进制表示中 1 的个数为 t 个,那么最终需要的总的切刀数即为
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
首要的目标就是找到一种最节约的方式使得
显然得考虑 DP 求解
观察题目的范围,可以
状态
定义
转移
先统计每个数的出现次数
设初始的时候
可以发现一点就是,如果将所有数
故
最终的答案即为
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 种位运算操作,很容易想到拆位考虑
对于每一位,由
题目中的给定的 30 个位中会包含几种
由此,可以发现,我们可以预处理出所有的转移,这样查询的时候考虑 30 位的限制即可
那么简单,利用 8 个数表示状态,再进行转移即可,但是这样没法考虑 30 位的限制。那我们将 8 种状态一起表示出来,利用一个 8 位的四进制数即可,再加上可能原输入的数对于某一种
五进制数 mask 的第 i 位表示
最小的操作次数可以利用 bfs 求解。初始五进制数为 mask = 33221100(五进制数),此时
这样我们遍历四种操作方式,记录最小值即可
遍历完之后需要再考虑每一个位上为 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 求解这个概率
状态:
转移:
初始均为 0,
那么排名大于一的边想要被成功选择,只能通过附加条件炸路使其变为排名第一的路
炸路存在两种情况:
一是选择了第一条边和另一条排名大于 j 的边,那么当前边的排名上升 1
二是选择了第一条边和另一条排名大于一小于 j 的边,那么当前边的排名上升 2
至此,预处理部分完成
由于原路都只能从前往后走,所以
//>>>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; }
本文来自博客园,作者:Qiansui,转载请注明原文链接:https://www.cnblogs.com/Qiansui/p/17742793.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!