计数 $dp$ $\&$ 期望 $dp$ $\&$ 数位 $dp$
\(DP\) 综合
计数 \(dp\)
组合数
期望 \(dp\)
例
\(n\) 个黑球 \(m\) 个白球 每次抓一个 放回 抓 \(k\) 次 求 \(6\) 次是黑球的概率
期望
\(Collecting\ Bugs\)
有 \(n\) 种 \(BUG\) \(s\) 个系统 每天发现一个系统的一个 \(BUG\) 假定每种 \(BUG\) 的个数无限多 问发现每种 \(BUG\) 且每个系统里发现了 \(BUG\) 的期望天数
\(f_{i, j}\) 表示发现了 \(i\) 种 \(BUG\) 有 \(j\) 个系统已经发生了 \(BUG\) 的期望天数
四种情况
- 发现一个无用的 \(BUG\)
- 发现一个新 \(BUG\) 在老系统中
- 发现一个老 \(BUG\) 在新系统中
- 发现一个新 \(BUG\) 在新系统中
两边都有 \(f_{i, j}\) 无法转移 移项 化简
换教室
\(floyd\) 求两点之间的最短路
考虑 \(f_{i, j, 0/1}\) 表示前 \(i\) 节课 申请了 \(j\) 节 第 \(i\) 节课有没有申请的最小期望总路程
/*
Time: 2.7
Worker: Blank_space
Source: P1850 [NOIP2016 提高组] 换教室
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const double INF = 0x3f3f3f3f;
const int FFF = 0x8fffffff;
/*------------------------------------常量定义*/
int n, m, v, e, mp[310][310], c[2021], d[2021];
double f[2021][2021][2], ans = INF, k[2021];
/*------------------------------------变量定义*/
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return x * f;
}
/*----------------------------------------快读*/
double min(double x, double y) {return x < y ? x : y;}
double max(double x, double y) {return x > y ? x : y;}
/*----------------------------------------函数*/
int main()
{
memset(mp, 0x3f, sizeof mp);
n = read(); m = read(); v = read(); e = read();
for(int i = 1; i <= n; i++) c[i] = read();
for(int i = 1; i <= n; i++) d[i] = read();
for(int i = 1; i <= n; i++) scanf("%lf", &k[i]);
for(int i = 1; i <= e; i++)
{
int x = read(), y = read(), z = read();
mp[x][y] = mp[y][x] = min(mp[x][y], z);
}
for(int l = 1; l <= v; l++)
for(int i = 1; i <= v; i++)
for(int j = 1; j <= v; j++)
mp[i][j] = min(mp[i][j], mp[i][l] + mp[l][j]);
for (int i = 1; i <= v; i++)mp[i][i] = mp[i][0] = mp[0][i] = 0;
for(int i = 0; i <= n; i++)
for(int j = 0; j <= m; j++) f[i][j][0] = f[i][j][1] = INF;
f[1][0][0] = f[1][1][1] = 0;
for(int i = 2; i <= n; i++)
{
f[i][0][0] = f[i - 1][0][0] + mp[c[i - 1]][c[i]];
for(int j = 1; j <= min(i, m); j++)
f[i][j][0] = min(f[i][j][0], min(f[i - 1][j][0] + mp[c[i - 1]][c[i]], f[i - 1][j][1] + k[i - 1] * mp[d[i - 1]][c[i]] + (1 - k[i - 1]) * mp[c[i - 1]][c[i]])),
f[i][j][1] = min(f[i][j][1], min(f[i - 1][j - 1][0] + k[i] * mp[c[i - 1]][d[i]] + (1 - k[i]) * mp[c[i - 1]][c[i]],
f[i - 1][j - 1][1] + k[i - 1] * k[i] * mp[d[i - 1]][d[i]]
+ (1 - k[i - 1]) * k[i] * mp[c[i - 1]][d[i]]
+ k[i - 1] * (1 - k[i]) * mp[d[i - 1]][c[i]]
+ (1 - k[i - 1]) * (1 - k[i]) * mp[c[i - 1]][c[i]]));
}
for(int i = 0; i <= m; i++) ans = min(ans, min(f[n][i][0], f[n][i][1]));
printf("%.2lf", ans);
return 0;
}
\(Red\ Is\ Good\)
桌面上有 \(n\) 张红牌和 \(m\) 张黑牌 随机打乱顺序后放在桌面上 开始一张一张地翻牌 翻到红牌得到一元 黑牌付出一元 可以随时停止翻牌 问最优策略下期望得到多少钱
自己 yy 了一个假式子
\(f_{i, j}\) 表示剩下 \(i\) 张红牌 和 \(j\) 张黑牌获得钱的期望
充电
有 \(n\) 个点 互相之间通过 \(n - 1\) 条边连接 第 \(i\) 个节点由 \(x_i\) 的概率直接充电 第 \(j\) 条边有 \(y_j\) 的概率导电 求期望有电的节点数
有电的难求 求每个点没有电的概率
\(f_u\) 表示不考虑父亲的情况下第 \(i\) 个点没电的概率
翻译成人话 大概就是 该点不考虑父节点没电的概率就是该点本身没电的概率乘上他所有孩子节点的 本身没电的概率乘上有电但是不导电的概率
用 \(g_i\) 表示第 \(i\) 个点的父亲没有向 \(i\) 个点导电的概率
设 \(fa_u\) 在不考虑 \(u\) 点的情况下 没电的概率为 \(t\)
有
翻译成人话 大概是 该点不考虑 \(u\) 点时没电的概率就是该点的父节点不向该点导电的概率 乘上 该点不考虑父亲时没电 的概率 比上 \(u\) 点没电的概率乘上有电但不导电的概率
\(i\) 没电的概率就是 \(f_i \times g_i\)
\(isn\)
给定一个长度为 \(n\) 的序列 \(A\) 如果 \(A\) 不是非降的 必须从中删去一个数 一直操作 直到 \(A\) 非降为止 求有多少种不同的操作方案 答案模 \(10^9 + 7\)
\(g_{i, j}\) 表示 以 \(j\) 结尾的长度为 \(i\) 的非降序列的个数 枚举下一个数 \(x\) 有
这里没有考虑删除的顺序 最后乘个阶乘得到 \(f_i\) 表示长度为 \(i\) 的非降序列个数
中途可能到某一步停止 容斥
\(OSU!\)
一共有 \(n\) 次操作 每次操作只有成功和失败之分 成功对应 \(1\) 失败对应 \(0\) \(n\) 次操作对应一个长度为 \(n\) 的 \(01\) 串 在这个串中连续的 \(x\) 个 \(1\) 可以贡献 \(x^3\) 的分数 这 \(x\) 个 \(1\) 不能被其他连续的 \(1\) 包含
给定 \(n\) 及每个操作的成功率 输出期望分数 保留一位小数
维护三个数组 直接转移
/*
Time: 3.14
Worker: Blank_space
Source: P1654 OSU!
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define Max(x, y) ((x) > (y) ? (x) : (y))
#define Min(x, y) ((x) < (y) ? (x) : (y))
#define Abs(x) ((x) < 0 ? -(x) : (x))
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n;
double x[B], x_2[B], ans[B];
/*------------------------------------变量定义*/
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return x * f;
}
/*----------------------------------------快读*/
/*----------------------------------------函数*/
int main() {
n = read();
for(int i = 1; i <= n; i++)
{
double p; scanf("%lf", &p);
x[i] = (x[i - 1] + 1) * p; x_2[i] = (x_2[i - 1] + 2 * x[i - 1] + 1) * p;
ans[i] = ans[i - 1] + (3 * x_2[i - 1] + 3 * x[i - 1] + 1) * p;
}
printf("%.1lf", ans[n]);
return 0;
}
\(Russian\ roulette\)
你和 \(n-1\) 个朋友玩俄罗斯轮盘赌
\(n\) 个人站成一圈 玩家 \(0\) 持枪 枪里有 \(c\) 个弹仓 有 \(n - 1\) 个子弹 游戏规则:
- 有枪的人朝自己开枪
- 没死 将枪传给顺时针数第 \(k\) 个人
- 死了 将枪传给顺时针下一个人
活到最后的获胜 求那个位置获胜概率最大
话说这朋友关系是有多好才会玩这种游戏啊喂
\(f_{i, j}\) 表示还剩 \(i\) 个人 当前 \(j\) 号人拿枪 枚举有没有打死自己转移
可能一直没打死自己导致枪转了一圈 此时会出现带环的转移 由于该 \(dp\) 为线性 最后可以写成 \(f_{i, j} = kf_{i, k} + b\) 的形式 其中 \(k\) 和 \(b\) 可以在 记忆化搜索是记录下来 当 \(j = k\) 时 得到一个关于 \(f_{i, j}\) 的等式 直接解
原谅我 太菜了
数位 \(dp\)
\(B-number\)
求 \(n\) 以内所有能被 \(13\) 整除且数字里包含 \(13\) 的数的个数
第一维表示当前第 \(i\) 位 第二维表示模 \(13\) 的余数 第三维表示是否卡上界 第四维表示已经出现 \(13\) 或当前位不为 \(1\) 未出现过 \(13\)
按位转移
花神的数论题
\(f_{i, j, k}\) 表示前 \(i\) 位 有 \(j\) 个 \(1\) 是否顶上界的不同方案数
枚举多少个 \(1\) 快速幂
/*
Time: 2.5
Worker: Blank_space
Source: P4317 花神的数论题
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define emm(x) memset(x, -1, sizeof x)
#define int long long
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e7 + 7;
const int INF = 0x3f3f3f3f;
const int FFF = 0x8fffffff;
/*------------------------------------常量定义*/
int n, li[55], f[55][55][2][55], ans[55], _ans = 1;
/*------------------------------------变量定义*/
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return x * f;
}
/*----------------------------------------快读*/
int min(int x, int y) {return x < y ? x : y;}
int max(int x, int y) {return x > y ? x : y;}
int dfs(int i, int sum, bool limit, int _sum)
{
if(!i) return _sum == sum;
if(~f[i][sum][limit][_sum]) return f[i][sum][limit][_sum];
int up = limit ? li[i] : 1, res = 0;
for(int j = 0; j <= up; j++) res += dfs(i - 1, sum, limit && j == up, _sum + (j == 1));
return f[i][sum][limit][_sum] = res;
}
int power(int x, int p)
{
int res = 1;
while(p)
{
if(p & 1) res = x * res % mod;
x = x * x % mod;
p >>= 1;
}
return res;
}
int solve(int x)
{
int t = 0;
while(x) li[++t] = x & 1, x >>= 1;
for(int i = 1; i <= 50; i++) emm(f), ans[i] = dfs(t, i, 1, 0);
for(int i = 1; i <= 50; i++) _ans = _ans * power(i, ans[i]) % mod;
return _ans;
}
/*----------------------------------------函数*/
signed main()
{
n = read();
printf("%lld", solve(n));
return 0;
}
某些插曲
上午的东西到这里结束
下午上课一开始讲的东西毫无听课体验
断网半个小时多 回放没录上 断断续续 大范围略过 根据增广矩阵推测是高斯消元相关
网络差评
\(Gotta\ Go\ Fast\)
\(f_{i, j}\) 表示当前在 \(i\) 关 已经花了 \(j\) 秒钟 期望还要多久打通
特判在 \(j + s_i\) 或 \(j + f_i\) 超过 \(R\) 时 直接从头开始
如何决策是否从头开始
二分 \(f_{0, 0}\) 的值 \(mid\) 若 $mid < $ 计算出的 \(f_{0, 0}\) 说明 \(mid\) 应该变大 否则 \(mid\) 应该变小 这样就解决了转移带环的问题 复杂度为 \(O(kn^2s_i)\) \(k\) 为二分次数
/*
Time: 5.15
Worker: Blank_space
Source: CF865C Gotta Go Fast
*/
/*--------------------------------------------*/
#include<cstdio>
#include<algorithm>
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, m, a[60], b[60], p[60];
double f[60][5010];
/*------------------------------------变量定义*/
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return x * f;
}
/*----------------------------------------快读*/
bool check(double x) {
for(int i = n - 1; ~i; i--)
{
for(int j = m + 1; j < 5000; j++) f[i + 1][j] = x;
for(int j = 0; j <= m; j++)
f[i][j] = std::min(x, (f[i + 1][j + a[i]] + a[i]) * p[i] * 1.0 / 100 + (f[i + 1][j + b[i]] + b[i]) * (100 - p[i]) * 1.0 / 100);
}
return f[0][0] < x;
}
/*----------------------------------------函数*/
int main() {
n = read(); m = read();
for(int i = 0; i < n; i++) a[i] = read(), b[i] = read(), p[i] = read();
double l = 0, r = 1e10, mid = (l + r) / 2;
for(int i = 1; i <= 100; i++, mid = (l + r) / 2)
if(check(mid)) r = mid; else l = mid;
printf("%.10lf", l);
return 0;
}
其他 \(dp\)
\(Emiya\) 家今天的饭
一个带点权的矩阵 选 \(k\) 个点 每行选一个 每列选的节点不超过所选节点的一半 不能不选 求所有方案中 选择的点权乘积的和
不考虑每列不超过一半的这个限制 求总方案数 然后减去考虑这个限制后不合法的方案数
求任意列选的节点超过所选节点的一半的方案数之和
在一个方案中 只可能有一列的节点超过所选的节点的一半 枚举超过限制的列 然后 \(dp\) 求解
\(——To\ be\ continue\)
放到了这里
考试技巧
如何打表
加入打 \(10^6\) 个数 每个数小于 \(10^9\) 考虑代码长度限制 \(64kb\)
打表占用内存太大
分块打表 将 \(10^9\) 分成每六千一个块
差分打表 数的差距比较小时 减小数的大小
搜索
大胆剪枝
\(dfs\) 的顺序非常重要
最优化剪枝
if(clock() / CLOCK_PER_SEC >= 0.9) return ;
if(++cnt >= 1e5) exit();
关于树剖
数据随机的情况下复杂度为 \(O(\log \log n)\)
完全二叉树可以卡树剖 但是一般不会卡 容易把暴力放跑
关于最短路
菊花图套链 ...
网格图卡 \(SPFA\)
\(dijk\) 复杂度 \(O((n + m)\log m)\)
稠密图中不加堆优化比堆优化要优一点
后略
\(——\ End\)

浙公网安备 33010602011771号