[OI] DP
温馨提示:Contest 大字的右侧有目录可以快速跳转
Contest
- 背包DP
- 线性DP
- 区间DP
- 坐标DP
- 树形DP
- 状压DP
- 概率与期望DP
- 计数DP
- 单调优化DP
背包DP
背包DP可以理解为“从若干物品中选择特定物品”的问题的统称. 对背包问题开dp数组,通常需要考虑以下几个维度
- 已选物品数量
- 背包容量
数组中通常存放该条件下的最大价值.
下面是一些常见的背包DP模型:
Ⅰ 0-1背包
Ⅱ 完全背包
Ⅲ 多重背包
Ⅳ 混合背包
Ⅴ 分组背包
Ⅵ 有依赖的背包
Ⅶ 背包方案数
Ⅷ 求背包方案
Ⅸ 背包问题第k优解
Ⅰ 0-1背包
背包问题基础状转公式: f[j]=f[j-v[i]]+w[i]
01背包第二层循环倒序执行
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;++i){
for(int j=m;j>=v[i];--j){
if(f[j]<f[j-v[i]]+w[i]){
f[j]=f[j-v[i]]+w[i];
}
}
}
cout<<f[m];
Ⅱ 完全背包
完全背包=01背包的第二层循环正序执行
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;++i){
for(int j=v[i];j<=m;++j){
if(f[j]<f[j-v[i]]+w[i]){
f[j]=f[j-v[i]]+w[i];
}
}
}
cout<<f[m];
Ⅲ 多重背包
关于多重背包一些坑点
①第二层循环正序和倒序确实都行
②二进制拆分循环为 while(c>base)
,写
③第一层循环是
cin>>m>>n;
for(int i=1;i<=n;++i){
int a,b,c,base=1;
cin>>a>>b>>c;
while(c>base){
v[++now]=a*base;
w[now]=b*base;
c-=base;
base*=2;
//或base<<=1;
}
v[++now]=a*c;
w[now]=b*c;
}
for(int i=1;i<=now;++i){
for(int j=v[i];j<=m;++j){
//或for(int j=m;j>=v[i];--j){
if(f[j]<f[j-v[i]]+w[i]){
f[j]=f[j-v[i]]+w[i];
}
}
}
cout<<f[m]<<endl;
Ⅳ 混合背包
①思路一伪代码:
if(是01背包) 01背包处理
if(是完全背包) 完全背包处理
if(是多重背包) 多重背包处理
②思路二:
将01背包拆成
思路二实现:
cin>>t>>n;
for(int i=1;i<=n;++i){
int base=1,p,b,c;
cin>>p>>b>>c;
if(c==0){
c=t/p;
}
while(c>base){
c-=base;
a[++anow].w=base*b;
a[anow].v=base*p;
base*=2;
}
a[++anow].w=b*c;
a[anow].v=p*c;
}
for(int i=1;i<=anow;++i){
for(int j=t;j>=a[i].v;--j){
if(f[j-a[i].v]+a[i].w>f[j]){
f[j]=f[j-a[i].v]+a[i].w;
}
}
}
cout<<f[t];
Ⅴ 分组背包
Ⅴ-Ⅰ 分组背包一:每组中的物品互相冲突,最多选一件
思路:把每组看成一个01背包,遍历每组求最优解.
也就是将01背包中的 “选择几个物品” 改为 “选择几个背包”,并且在每组背包中选择一个尽可能最优的.
cin>>mv>>n>>t;
for(int i=1;i<=n;++i){
int x,y,z;
cin>>x>>y>>z;
size[z]++;
v[z][size[z]]=x;
w[z][size[z]]=y;
}
for(int i=1;i<=t;++i){//zushu
for(int j=1;j<=mv;++j){//v
f[i][j]=f[i-1][j];
for(int k=1;k<=size[i];++k){//member
if(j>=v[i][k]&&f[i-1][j-v[i][k]]+w[i][k]>f[i][j]){
f[i][j]=f[i-1][j-v[i][k]]+w[i][k];
}
}
}
}
cout<<f[t][mv];
Ⅴ-Ⅱ 分组背包二:每组中的物品不冲突,每组至少选一件
Ⅵ 有依赖的背包
有依赖的背包也叫树上背包,详见 树上背包问题.
Ⅶ 背包方案数
对方案数问题,我们可以考虑开一个
Ⅷ 求背包方案
思路:开
伪代码①:
if(状态转移成功) pre[j]=i;
...
while(最后的点t){
...
t-=v[pre[t]];
}
伪代码②:
if(状态转移成功) pre[j]=i;
...
while(最后的点t){
...
t=pre[t];
}
Ⅸ 背包问题第k优解
例题-Bone Collector II
思路:增加一维,用来记录第k优解。每次状态转移时都将计算所得全部的值(k个数状态转移后的全部nk种情况)合并后取前k个合并,这样就可以维护一个k优解数组。
以 Bone Collector II 中 01 背包第k优解为参考:
cin>>n>>m>>k;
for(int i=0;i<n;i++){
cin>>w[i];
}
for(int i=0;i<n;i++){
cin>>v[i];
}
for(int i=0;i<n;i++){
for(int j=m;j>=v[i];j--){
int p,x,y,z;
for(p=1;p<=k;p++){ //对k个数进行状态转移
a[p]=f[j-v[i]][p]+w[i];
b[p]=f[j][p];
}
a[p]=b[p]=-1; //二分合并
x=y=z=1;
while(z<=k&&(a[x]!=-1||b[y]!=-1)){
if(a[x]>b[y]){
f[j][z]=a[x++];
}
else{
f[j][z]=b[y++];
}
if(f[j][z]!=f[j][z-1]){
z++;
}
}
}
}
cout<<f[m][k];
线性DP
线性DP较为灵活,无法系统分为几大类,但大体有下面几种模型:
Ⅰ 最长序列问题
Ⅱ 带捆绑的最长序列问题
Ⅲ 求最少拆链问题
Ⅳ 最长公共问题
Ⅰ 最长序列问题
Ⅰ-Ⅰ 概述
设有由
最长序列问题的基本公式:f[i]=f[j]+1 (a[i] \operator\ a[j],i from n to 1,j from 1 to n)
关于符号:
①最长上升
②最长下降
③最长不上升
④最长不下降
另外,为了方便说明,我们把在这里的①③,②④分别叫做相反的最长序列
原理:假如在
这里以最长上升序列给出代码:
for(int i=n;i>=1;--i){
next=0;
for(int j=i+1;j<=n;++j){
if((a[j]>a[i])&&(f[j]+1)>f[i]){
f[i]=f[j]+1;
next=j;
}
}
if(maxn<=f[i]){
maxn=f[i];
head=i;
}
pre[i]=next;//pre用于记录路径,不需要时可不加
}
cout<<f[head]<<endl;
Ⅰ-Ⅱ 更优的最长上升子序列问题
上述做法为
考虑到对于每次转移,我们都可以记录下当前序列最末尾的数字,可以想到的是,当前序列末尾数字越小,则可能被填入子序列的数字就越多,因此末尾更小的序列可能就是决策更优的,因此我们每次都保留这样的序列向后拓展.
也就是,我们使用数组
- 当新的元素值更大的时候,符合上升条件,直接填到末尾
- 当新的元素值更小的时候,虽然不符合上升条件,但是符合更新条件,可能会导致更优的决策,因此我们向前找到第一个能放当前值的
进行更新.
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n;
int a[100001];
int f[100001];
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
}
memset(f,0x3f,sizeof f);
f[1]=a[1];
int len=1;
for(int i=2;i<=n;++i){
int l=0,r=len;
if(a[i]>f[len]){
f[++len]=a[i];
}
else{
f[lower_bound(f+1,f+len+1,a[i])-f]=a[i];
}
}
cout<<len<<endl;
}
Ⅱ 带捆绑的最长序列问题
有两组数列一一对应,选出一些数让两边数列都具有单调性,求序列最大值
友好城市
这类问题的统一解决方式:
①存成结构体按一个变量sort
②对另外的变量执行最长序列
sort(a+1,a+n+1);
cout<<upperlenth(1,n);
其中:
int upperlenth(int l,int r){
for(int i=l;i<=r;++i){
f[i]=1;
}
int maxn=1,head=0,next=0;
for(int i=r;i>=l;--i){
next=0;
for(int j=i+1;j<=r;++j){
if((a[j].r>a[i].r)&&(f[j]+1)>f[i]){
f[i]=f[j]+1;
next=j;
}
}
if(maxn<=f[i]){
maxn=f[i];
head=i;
}
}
return f[head];
}
Ⅲ 求最少拆链问题
有一组数列,求最少能把它们分成几组有单调性的数列
例题-拦截导弹Ⅱ
有一定理:最少拆链=该序列的相反最长序列
Ⅳ 最长公共问题
最长公共问题有最长公共子串、最长公共子序列和最长公共前缀三类
Ⅳ-Ⅰ 最长公共子串问题
定义:字串是一个字符串中连续的一段,公共子串即为几个字符串都含有的子串.
我们求的是两个字符串的公共子串,所以应该意识到这个 dp 方程是个二维数组
对于 dp[i][j] 来说,它的值有两种可能,取决于字符 x[i] 和 y[j] 这两个字符是否相等
如果两个字符相等,则
如果
综上,我们的状态转移方程如下:
Ⅳ-Ⅱ 最长公共子序列问题
定义:字序列是一个字符串中有序的一段,即序列中的每个数在原序列内都从左到右排列,公共子序列即为几个字符串都含有的子序列.
类似最长公共子串,但不一样的是,假如
Ⅳ-Ⅲ 排列的最长公共子序列问题
假如序列中的每个数都仅仅出现过一次,那么我们可以考虑对上述
A:3 2 1 4 5
B:1 2 3 4 5
我们不妨给它们重新标个号:把
A: a b c d e
B: c b a d e
这样标号之后,最长公共子序列长度显然不会改变. 但是出现了一个性质:
两个序列的子序列,一定是
换句话说,只要这个子序列在
哪个最长呢?当然是
因此,我们只需要在
区间DP
区间DP主要用于能将大问题划分成小段区间的问题当中,一般来说,区间DP有这样几种维度可以枚举.
- 区间长度
- 起点位置
- 断开位置
给出一个大致模板:
//首先预处理(区间DP中预处理真的很重要,特别是对于长度为 1 或 n 的特殊区间)
for(int len=2;len<=n;++len){ //从小到大枚举区间长度,len=2因为已经预处理了
for(int i=1;i+len-1<=n;++i){ //枚举起点
int j=i+len-1;
for(int k=i;k<=j=1;++k){ //枚举划分点
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+cost);
//从k点合并,cost是合并费用
}
}
}
可以看出,区间DP就是一个不断合并的过程. 下面列举一些区间DP的经典例题:
Ⅰ 石子合并
Ⅱ 环形问题的处理
Ⅲ 整数划分
Ⅳ 凸多边形的三角剖分
Ⅰ 石子合并
Ⅰ-Ⅰ 题目描述
有
堆石子排成一排( ),现要将石子有次序地合并成一堆,规定每次只能选相邻的两堆合并成一堆,并将新的一堆的石子数,记为改次合并的得分,编一程序,由文件读入堆数 及每堆石子数( ).
(1)选择一种合并石子的方案,使得做
(2)选择一种合并石子的方案,使得做
这是一道需要我们合并的区间DP题,常规做法求解即可,在这里提到一个小知识:
Ⅰ-Ⅱ 前缀和求区间和
前缀和
Ⅰ-Ⅲ 代码实现
点击查看代码
for(int i=1;i<=n;++i){
cin>>a[i];
s[i]=s[i-1]+a[i];
}
memset(f[1],0x3f,sizeof(f[1]));
for(int i=1;i<=n;++i){
f[1][i][i]=0;
}
for(int len=2;len<=n;len++){
for(int i=1;i<=n-len+1;i++){
int j=i+len-1;
for(int k=i;k<j;k++){
f[1][i][j]=min(f[1][i][j],f[1][i][k]+f[1][k+1][j]+s[j]-s[i-1]);
f[0][i][j]=max(f[0][i][j],f[0][i][k]+f[0][k+1][j]+s[j]-s[i-1]);
}
}
}
cout<<f[1][1][n]<<endl<<f[0][1][n];
Ⅱ 环形问题的处理
现在我们想一个问题,假如石子排列成一个环形,那我们要怎么处理.
环形数据结构的正确解法应该是:将原数组复制一份,然后接到原数组后面
这样做为什么是对的,因为环形虽然首尾相接,但不会出现绕了超过一圈被更新的情况.
访问或建立时,我们可以直接使用模
Ⅲ 整数划分
用
其中
那么现在我们仍然有一个问题没有解决:如何求
其实很简单,只需要这样就行
点击查看代码
void ton(const string &x){
for(int len=1;len<=x.length();++len){
for(int i=1;i<=x.length()-len+1;++i){
for(int j=i;j<=i+len-1;++j){
a[i][i+len-1]=a[i][i+len-1]*10+x[j-1]-'0';
}
}
}
}
Ⅳ 凸多边形的三角剖分
给定一具有
个顶点(从 到 编号)的凸多边形,每个顶点的权均已知。问如何把这个凸多边形划分成 个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小?
根据区间DP的思想,我们规定区间
另外,本题的初始化应该从三角形的顶点权乘积可求来下手,即:
再说一句,矩阵连乘这道题与本题目代码一致,它的题目如下:
一个
矩阵由 行 列共 个数排列而成。两个矩阵 和 可以相乘当且仅当 的列数等于 的行数。一个 的矩阵乘以一个 的矩阵等于一个 的矩阵,运算量为 。 矩阵乘法满足结合律, 可以表示成 或者是 ,两者的运算量却不同。例如当 时, 而 。显然第一种顺序节省运算量。 现在给出 个矩阵,并输入 个数,第 个矩阵是 ,求最优的运算量.
坐标DP
坐标DP本身和区间DP有许多相似之处,比如都用一些类似坐标的信息来定义DP数组. 但是两者不同的是,区间DP是通过小区间判断大区间,而坐标DP是通过其他坐标判断当前坐标. 总的来说,坐标DP有下面几种经典的问题.
Ⅰ 传纸条-路线往返求最大价值
Ⅱ 晴天小猪历险记之Hill-三角形的最大价值路线
Ⅲ 免费馅饼-时空坐标轴
Ⅳ 盖房子-阻碍联通的分型图最大面积
Ⅴ 矩阵取数游戏
Ⅰ 传纸条-路线往返求最大价值
对于此类问题,我们不妨将两次行动一起DP,只需要判断两个轨迹不重复即可,因为是二维地图,很容易想到二维dp数组,但实际上还要再加两维(因为是两次行动一起DP),状态转移方程为
但是这样做很麻烦,注意到若设步数为
实际上,因为第一维全都是
点击查看代码
for(int k=1;k<=m+n-3;++k){
for(int i=0;i<=k;++i){
for(int j=0;j<=k;++j){
if(i==j){
continue;
}
f[k][i][j]=
max(
max(f[k-1][i][j],f[k-1][i-1][j]),
max(f[k-1][i][j-1],f[k-1][i-1][j-1])
)
+h[i][k-i]+h[j][k-j];
}
}
}
cout<<f[m+n-3][m-1][m-2];
Ⅱ 晴天小猪历险记之Hill-三角形的最大价值路线
对于上图这样的经典三角形,求从下到上的最大价值路线.
和其他三角图形题的做法类似,我们先将三角形进行左对齐.
我们发现,每一个上层节点都可以由其下层节点而来,即:
这样我们从下向上遍历(以坐标来看是从大到小dp)就可以求出最大价值路线
实际上,所有这样的问题都可以使用最短路解决
Ⅲ 免费馅饼-时空坐标轴
对于这样带有时间维度的题目,分时间DP似乎不是明智之举,那么我们为什么不算出每个物品到达的时刻,然后以时刻为纵坐标进行坐标DP呢.
点击查看代码
while(cin>>a>>b>>c>>d){
if((h-1)%c==0){
s[a+(h-1)/c][b]+=d;
}
}
for(int i=1;i<=100;++i){
for(int j=1;j<=w;++j){
f[i][j]=max({f[i-1][j],f[i-1][j-1],f[i-1][j+1],f[i-1][j-2],f[i-1][j+2]})+s[i][j];
}
}
cout<<f[100][1];
Ⅳ 盖房子-阻碍联通的分型图最大面积
此类问题我在其他文章中已说明,详见 盖房子 hzoi-tg#262
另外,给出一道三角形分型的此类问题解析 三角蛋糕 hzoi-tg#261
Ⅴ 矩阵取数游戏
矩阵取数游戏是坐标DP一道十分经典的题目,详见 矩阵取数游戏<简单版> hzoi-tg-906-2
树形DP
树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的.
下面给出树形DP的基本框架:
void dfs(int s){
for(遍历全部子节点){
dfs(子节点);
进行本层dp;
}
}
树形DP的结构不算太难,主要难点只集中于两个方面.
Ⅰ 存图
Ⅱ 树上背包问题
Ⅰ 存图
树形DP的存图一般都是存树,但是有时候不好判断父子关系,所以我们存无向图,假如以无向图形式存储,那么我们只需要保证dfs时,当前节点不会返回它来的地方即可. 我们给dfs加一个参数
至于二叉树的存储,通常使用 dfs ,详见 三色二叉树 hzoi-tg#282 存图方法
Ⅱ 树上背包问题
详见 树上背包问题
状压DP
Ⅰ 概述
状态压缩 DP 其实大部分都属于其他 DP 的范畴,即状态转移方程很好推. 状压的主要难点是在它的状态可能有很多. 比如下面的例题.
在一个国际象棋棋盘中放
个国王,使它们都不能互相吃. 问方案数.
这个题我们想要 DP,那么需要记录当前这一行的每个位置是否放了棋子,但是当
因此,我们想要开下这样的数组,就需要将这样的状态压缩进同一个变量里.
假如我们用 “状态的二进制表示中第
我们这样做还有一个优势,就是在进行不同行比较的时候可以使用位运算快速比较. 下面给出一些比较的例子:
判断该行第
(a>>i)&1==1
判断该行与下一行是否相同
!(a^b)
判断状态
(a&b)==b
或者 (a|b)==a
判断状态
(a&(b<<1)||(a&(b>>1)))
这样,我们通过枚举状态即可得到答案.
以例题为例给出代码:
点击查看代码
#include<stdio.h>
using namespace std;
typedef long long ll;
int n,k,top=0;
int c[1<<10],s[1<<10];
void dfs(int cond,int sum,int pos){
if(pos>n){
c[++top]=cond;
s[top]=sum;
return;
}
dfs(cond+(1<<pos-1),sum+1,pos+2);
dfs(cond,sum,pos+1);
}
ll f[11][1<<10][31];
int main(){
scanf("%d%d",&n,&k);
dfs(0,0,1);
for(int i=1;i<=top;++i) f[1][c[i]][s[i]]=1ll;
for(int i=2;i<=n;++i)
for(int j=1;j<=top;++j)
for(int h=1;h<=top;++h){
if(c[j]&c[h]) continue;
if((c[j]<<1)&c[h]) continue;
if((c[j]>>1)&c[h]) continue;
for(int sum=k;sum>=s[j];--sum)
f[i][c[j]][sum]+=f[i-1][c[h]][sum-s[j]];
}
ll ans=0ll;
for(int i=1;i<=top;++i) ans+=f[n][c[i]][k];
printf("%lld",ans);
return 0;
}
Ⅱ 例题:Tourist Attractions
题目描述
给出一张有
你需要找一条从
每个限制条件形如
注意,这里的停留不是指经过。
解法分析
对于这道题的状压. 我们考虑枚举 "现在已经在哪些点停留" 这样一种状态. 然后去寻找每一个当前未停留的点,考虑这个点的前置节点是否全部已经停留,如果是,那么枚举每一个已在集合内的节点,尝试把这个点通过某条边放入集合内,进行状态转移.
那么我们需要进行状态压缩的有两个东西: 现在已有的点的情况 (用于枚举) 和每个点的前置节点情况 (用于判断).
最后需要我们输出的就是在全部节点停留情况下的状态.
这道题的主要思路:
for(int i=1;i<=(1<<k)-1;++i){
//all possible chance
for(int j=0;j<=k-1;++j){ //node
if(i&(1<<j)){
//if this node in this chance
for(int hdk=0;hdk<=k-1;++hdk){
//then try to add a node
if(!(i&(1<<hdk))&&((i|r[hdk+2])==i)){
//if find a node not in chance and can be placed
update();
}
//then do the change. why it's +2 is because I store 3 in position 1.(1 and 2 is no need)
}
}
}
}
那么,为了更新已选中的点的距离,我们需要知道从任意点到另一点的距离,也就是跑一边全源最短路.
我们定义
那么我们怎么表示
这题我也不知道它卡什么. 我存图的 vector 滚动数组会比前向星小很多,而 DIJ 又比 SPFA 快很多. 总之按对的来吧.
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,k;
struct edge{
int to,w;
};
struct node{
int id,dis;
bool operator<(const node A)const{
return dis>A.dis;
}
};
priority_queue<node> q;
vector<edge> e[20001];
int dis[31][20001]; //root i's dis j
bool vis[20001];
void dij(int s){
memset(vis,false,sizeof vis);
for(int i=1;i<=n;++i){
dis[s][i]=1000000000;
}
while(!q.empty()) q.pop();
dis[s][s]=0;
q.push(node{s,0});
while(!q.empty()){
node u=q.top();
q.pop();
if(vis[u.id]) continue;
vis[u.id]=true;
for(edge i:e[u.id]){
if(dis[s][i.to]>dis[s][u.id]+i.w){
dis[s][i.to]=dis[s][u.id]+i.w;
q.push(node{i.to,dis[s][i.to]});
}
}
}
}
int f[1<<20][31],r[31],ans=1000000000; //1=zt(which nodes been chose) 2=node //r= mustfore
int main(){
cin>>n>>m>>k;
for(int i=1;i<=m;++i){
int a,b,c;
cin>>a>>b>>c;
e[a].push_back(edge{b,c});
e[b].push_back(edge{a,c});
}
if(k==0){
dij(1);
cout<<dis[1][n];
return 0;
}
int q;
cin>>q;
while(q--){
int a,b;
cin>>a>>b;
r[b]+=(1<<(a-2));//
}
for(int i=1;i<=k+1;++i){
dij(i);
}
for(int i=0;i<=(1<<k)-1;++i){
for(int j=1;j<=k+1;++j){
f[i][j]=1000000000;
}
}
f[0][1]=0;
for(int i=2;i<=k+1;++i){
if(!r[i]){ //if this point hasn't any requires.
f[1<<(i-2)][i]=dis[1][i];
}
}
for(int i=1;i<=(1<<k)-1;++i){ //all possible chance
for(int j=0;j<=k-1;++j){ //node
if(i&(1<<j)){ //if this node in this chance
for(int hdk=0;hdk<=k-1;++hdk){ //then try to add a node
if(!(i&(1<<hdk))&&((i|r[hdk+2])==i)){ //if find a node not in chance and can be placed
f[i|(1<<hdk)][hdk+2]=min(f[i|(1<<hdk)][hdk+2],f[i][j+2]+dis[j+2][hdk+2]);
} //then do the change. why it's +2 is because I store 3 in position 1.(1 and 2 is no need)
}
}
}
}
for(int i=2;i<=k+1;++i){
ans=min(ans,f[(1<<k)-1][i]+dis[i][n]);
}
cout<<ans;
}
概率与期望DP
Ⅰ 关于概率与期望
从数学角度来说,事件
比如掷骰子,设结果为
对于概率与期望,一个很重要的性质是它们是可以分段计算的. 这里的分段计算,一部分是指可以分成几小部分分别计算然后加起来,还有另一部分是像计算复合函数
另外,还有非常重要的关于递推顺序的口诀:概率正推,期望逆推.
下面分别用搜索例题和递推例题来解释具体方法.
Ⅱ 搜索:扑克牌
题目描述
把一副扑克牌(
然后 从上往下依次翻开每张牌,每翻开一张黑桃、红桃、梅花或者方块,就把它放到对应花色的堆里去.
得到
特殊地,如果翻开的牌是大王或者小王,将会把它作为某种花色的牌放入对应堆中,使得放入之后期望值尽可能小.
题目分析
概率与期望DP的搜索一般都是记忆化搜索. 也属于动态规划的一种.
根据概率正推,期望逆推,我们首先设计递归最后一层的答案. 即已经翻出我们需要的牌的情况下期望是多少,很显然是
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define inf 100.0
double f[14][14][14][14][5][5];
int a,b,c,d;
double dfs(int q,int w,int e,int r,int dw,int xw){
if(f[q][w][e][r][dw][xw]>0) return f[q][w][e][r][dw][xw];
// cout<<"dfs "<<q<<" "<<w<<" "<<e<<" "<<r<<" "<<dw<<" "<<xw<<endl;
int cnt[5]={0,q,w,e,r};
cnt[dw]++;cnt[xw]++;
if(cnt[1]>=a&&cnt[2]>=b&&cnt[3]>=c&&cnt[4]>=d){
f[q][w][e][r][dw][xw]=0;
return 0;
}
int sum=54-cnt[1]-cnt[2]-cnt[3]-cnt[4];
if(sum<=0){
f[q][w][e][r][dw][xw]=inf;
return inf;
}
double res=1;
if(q<13) res+=((13.0-q)/sum)*dfs(q+1,w,e,r,dw,xw);
if(w<13) res+=((13.0-w)/sum)*dfs(q,w+1,e,r,dw,xw);
if(e<13) res+=((13.0-e)/sum)*dfs(q,w,e+1,r,dw,xw);
if(r<13) res+=((13.0-r)/sum)*dfs(q,w,e,r+1,dw,xw);
if(!dw){
double greater=inf;
for(int i=1;i<=4;++i){
greater=min(greater,(1.0/sum)*dfs(q,w,e,r,i,xw));
}
res+=greater;
}
if(!xw){
double greater=inf;
for(int i=1;i<=4;++i){
greater=min(greater,(1.0/sum)*dfs(q,w,e,r,dw,i));
}
res+=greater;
}
f[q][w][e][r][dw][xw]=res;
return res;
}
int main(){
cin>>a>>b>>c>>d;
memset(f,-1,sizeof f);
if(a+b+c+d>54){
cout<<"-1.000";return 0;
}
double ans=dfs(0,0,0,0,0,0);
printf("%.3lf",(ans>inf-1e-8? -1:ans));
}
Ⅲ 递推:卡牌游戏
题目描述
这里有一个简单的例子:
例如一共有 3,4,5,6
.
第一回合,庄家是玩家 1,2,3,4,1
,最后玩家
第二回合,庄家就是玩家 2,3,4,2,3,4
,玩家
第三回合,玩家
题目分析
这是一道经典的期望递推DP. 其实二者的本质是一样的,但一般来说递推DP更难一些.
对于这道题,不妨以 剩下的人数+庄家 作为状态,枚举庄家,枚举牌,算出可能的情况,然后加上对应的期望,最后倒序加和即可.
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define div *1.0/
int n,m;
int k[51];
double f[51][51]; //host j the winp i, while remain k
inline int deadman(int tot,int tar){
return tar%(tot+1);
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;++i){
cin>>k[i];
}
f[1][1]=1 div 1;
for(int i=2;i<=n;++i){ //size
for(int j=1;j<=m;++j){ //choose
int p=(k[j]%i==0? i:k[j]%i);
for(int k=1;k<=i-1;++k){
if(++p>i) p=1;
f[i][p]+=f[i-1][k] div m;
}
}
}
for(int i=1;i<=n;++i){
printf("%.2lf%% ",100*f[n][i]);
}
}
实际上,概率与期望的灵活性十分高,需要注意其与其他类型DP,数据结构与博弈论的结合考察.
Ⅳ 二项式期望 DP
OSU
OSU 的题目是这样的:有一些相邻的块,给定每一个块的联通概率,每个连通块对答案有
关于此题我曾写过题解 此处
此类题的关键之处在于,当我们设计了一个线性状态
引入两个变量
容易想到
考虑
因为
因此,我们只需要维护出
下面我们来加上概率考虑
期望有一个性质(期望的线性性)
对于
否则,对于
否则,对于随机选择的块,直接用上述两种情况乘对应的概率即可,即:
注意到我们还没有维护
对于
对于
否则,按照上述思路,应为
接着考虑维护
对于
对于
否则,按照上述思路,应为
因为已经维护过了
#include<bits/stdc++.h>
using namespace std;
int n;
double p[100001],l1[100001],l2[100001],s[100001];
int main(){
cin>>n;
for(int i=1;i<=n;++i){
cin>>p[i];
}
for(int i=1;i<=n;++i){
l1[i]=p[i]*(l1[i-1]+1);
l2[i]=p[i]*(l2[i-1]+2*l1[i-1]+1);
s[i]=(1-p[i])*s[i-1]+p[i]*(s[i-1]+3*l2[i-1]+3*l1[i-1]+1);
}
printf("%.1lf",s[n]);
}
Another OSU
OSU 的
这一次我们不能再像上述一个一个推式子了,我们需要找一个普遍的规律:
对于刚才的问题我们发现:要想维护一个
有二项式定理,即
可以发现在这里实际上用到了全部次数比它低的
此外,除了用二项式定理求
杨辉三角递推式:
计数DP
Ⅰ 计数DP简述
计数 DP 是一类求方案数的DP,但是不一样的是,计数DP需要处理的问题往往十分复杂,使用普通的线性 DP 会超时. 这种时候就可以用计数 DP 进行解决.
实际上,计数 DP 的核心思想就是“用远处的状态来更新当前状态,进而减少时间开销”.
一般的计数 DP 是使用容斥原理+排列组合实现转移的. 也有特例. 下面将分别选择两种基本 DP 思路的计数 DP 进行讲解.
Ⅱ 排列组合计数-Gerald and Giant Chess
给定一个
的棋盘,棋盘上只有 个格子是黑色的,其他格子都是白色的. 在棋盘左上角有一个卒,每一步可以向右或者向下移动一格,并且不能移动到黑色格子中. 求这个卒从左上角移动到右下角,一共有多少种可能的路线. 很大, 很小
因为这题
为了递推的无后效性,我们把黑色格子按行再列优先的顺序从小到大排序,接着考虑构造不合法状态,于是设
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
int h,w,n;
int f[2001];
struct node{
int x,y;
bool operator<(const node &A)const{
if(x==A.x) return y<A.y;
return x<A.x;
}
}a[2001];
long long fact[1000001];
const int p=1e9+7;
long long power(long long a,long long n){
long long ans=1;
while(n){
if(n&1)ans=ans*a%p;
a=a*a%p;
n>>=1;
}
return ans;
}
void dofact(int n){
fact[0]=1;
for(int i=1;i<=n;++i){
fact[i]=fact[i-1]*i%p;
}
}
long long C(long long n,long long m){
if(!m){
return 1;
}
if(n<m){
return 0;
}
if(m>n-m){
m=n-m; //C{n}{m}=C{n}{n-m};
}
long long ans=fact[n]*power(fact[m],p-2)%p*power(fact[n-m],p-2)%p;
return ans;
}
long long lucas(long long n,long long m){
if(!m) return 1;
return lucas(n/p,m/p)*C(n%p,m%p)%p;
}
signed main(){
cin>>h>>w>>n;
for(int i=1;i<=n;++i){
cin>>a[i].x>>a[i].y;
}
dofact(h+w);
sort(a+1,a+n+1);
for(int i=1;i<=n;++i){
f[i]=lucas(a[i].x+a[i].y-2,a[i].x-1);
for(int j=1;j<i;++j){
if(a[j].x<=a[i].x&&a[j].y<=a[i].y){
f[i]-=f[j]*lucas(a[i].x+a[i].y-a[j].x-a[j].y,a[i].x-a[j].x);
f[i]=(f[i]+p)%p;
}
}
}
int ans=lucas(h+w-2,h-1);
for(int i=1;i<=n;++i){
ans-=(f[i]*lucas(h+w-a[i].x-a[i].y,h-a[i].x))%p;
ans=(ans+p)%p;
}
cout<<ans;
}
Ⅲ 位次问题转方案数-A decorative fence
这题爆搜肯定是不行,考虑一个事实:
假设 1xxxx 是
的一种方案,1yyyy 是 的另一种方案,而目标 满足 ,则有 的方案的首位一定是 .
跟我们猜数是一样的. 假设有个数给你猜,
所以我们就开个数组来转移并维护这个
注意到
设
那么这个转移非常好写,也不是本题的难点.
这里唯一需要注意的是求和的范围. 因为我们这个
那么还很容易注意到,这个转移和
然后就是按上面的思想来逼近我们要求的答案.
先来确定第一位吧,我们需要做的就是遍历每个
很显然,当我们之前几位选过某个数字,那我们就不能再选了,因此在之后的几次逼近中,我们还需要判断当前
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define speed ios::sync_with_stdio(false);
#define tests int cases;cin>>cases;while(cases--)
#define clear(i) memset((i),0,sizeof (i))
int f[21][21][2]; //now fences have n planks, and the leftest planks ranking j
//k=0 means leftest is shorter,else taller
int n,m;
bool vis[21];
/*
f[i][j][1]=sum k{from 1 to j-1} f[i-1][k][0]
f[i][j][0]=sum k{from j to i-1} f[i-1][k][1]
*/
void prework(){
f[1][1][1]=1;f[1][1][0]=1;
for(int i=2;i<=20;++i){
for(int j=1;j<=i;++j){
for(int k=1;k<=j-1;++k){
f[i][j][1]+=f[i-1][k][0];
}
for(int k=j;k<=i-1;++k){
f[i][j][0]+=f[i-1][k][1];
}
}
}
}
signed main(){
prework();
speed tests{
cin>>n>>m;
clear(vis);
int now,last;
for(int i=1;i<=n;++i){
if(f[n][i][1]>=m){
last=i;now=1;break;
}
else{
m-=f[n][i][1];
}
if(f[n][i][0]>=m){
last=i;now=0;break;
}
else{
m-=f[n][i][0];
}
}
cout<<last<<" ";
vis[last]=true;
for(int i=2;i<=n;++i){
now=1-now;int rank=0;
for(int len=1;len<=n;++len){
if(vis[len]) continue;
rank++;
if((now==0 and len<last)or(now==1 and len>last)){
if(f[n-i+1][rank][now]>=m){
last=len;break;
}
else{
m-=f[n-i+1][rank][now];
}
}
}
vis[last]=true;
cout<<last<<" ";
}
cout<<endl;
}
}
单调优化 DP
形如
for(int i=1;i<=n;++i){
for(int j=i;j<=n;++j){
for(int k=1;k<=j;++k){
f[i][j]=max(f[i][j],f[i][k]+w[k]);
}
}
}
显然它是
通过观察,可以发现这个式子有两个特殊性质:
的值域:- 状态转移:继承的状态中不会同时存在
项,比如假如我们的状态转移方程是 ,那这个题就没办法优化了,因为每次更新 的时候都会把所有值更新一遍
第一点,
发现当
下面我们来考虑如何去维护这个最值
开一个双端队列,用队首的元素来表示 “全部
- 如果队尾小于
,那么它一定不可能是最优的,因此从队尾出队 - 循环执行,直到队列为空或者队首更大
- 向队尾加入
这样可以保证队首的元素一定是最大的,因此我们对每次
下面我们来讨论一些特殊的状态转移方程的处理方式
第一类
对于这类式子,可以发现我们队列里的元素是具有时效性的,因此不能一直在队列里呆着
考虑到队尾元素并不影响更新,因此我们在队头进行操作:
- 每次检查队头元素是否满足
这个条件,如果不满足,则直接从队首出队
第二类
这里的状态转移式的后半部分是与
实际上,这个状态转移方程可以转化为这样:
做单调队列优化需要记住的两点
- 哪些变量与
有关,我们就将它放入优先队列中 - 假如状态式中存在一个同时由
影响的变量,则该状态转移方程无法用单调队列来优化
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!