update: 2021/6/25
文章目录
动态规划
线性DP
MaxSumPlusPlus
解释
状态表示 dp(i,j)表示前j个分成i组且以j结尾,获得的最大值。
目标状态 max(dp(m,i),…) i ≤ n , i ≥ m i \le n , i\ge m i≤n,i≥m
初始化 全为0
转移方程 dp(i,j) = max(dp(i,j - 1)+ a(j),dp(i - 1,k) + a(j)) 其中 k ≥ i − 1 , k ≤ j − 1 k\ge i-1,k\le j-1 k≥i−1,k≤j−1
二维写出来基本是这样,无论是时间还是空间,都很差。
signed main(){
int m,n;
while(cin>>m>>n){
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++){
for(int j=i;j<=n;j++){
int maxx = -1e18;
for(int k=i-1;k<j;k++) maxx = max(maxx,dp[i-1][k]);
if(j == i) dp[i][j] = maxx + a[j];
else dp[i][j] = max(dp[i][j-1],maxx) + a[j];
}
}
int ans = -1e18;
for(int i=m;i<=n;i++) ans = max(ans,dp[m][i]);
cout<<ans<<endl;
}
return 0;
}
时间优化
对于这个
for(int k=i-1;k<j;k++) maxx = max(maxx,dp[i-1][k]);
因为j是递增的,每次dp(i-1,k) 的前半部分都会重复计算。所以可以每次存一次就行。
用f(j - 1)表示dp(i - 1,j - 1)中的最大值。再将dp(i,j)降维。
取最后一次处理的最大值,即为答案。
代码
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10;
int dp[N],f[N],a[N];
signed main(){
IOS
int m,n;
while(cin>>m>>n){
memset(dp,0,sizeof(dp));
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++) cin>>a[i];
int maxx;
for(int i=1;i<=m;i++){
maxx = -1e18;
for(int j=i;j<=n;j++){
dp[j] = max(dp[j-1],f[j-1]) + a[j];
f[j-1] = maxx;
maxx = max(maxx,dp[j]);
}
}
cout<<maxx<<endl;
}
return 0;
}
更多例题
HDU1087 简单的DP转移方程,dp(i)表示以a(i)结尾时,所得的最大值。dp(i)可以由所有比它小的转移得到 代码
花店橱窗 注意,每一个花都需要放进花瓶,每一个花瓶最多有个,最少0个。代码
DAG上的DP
MonkeyandBanana
解释
每一个块可以有三种放置方式,只要a的底面两条边严格小于b,那么就可以将它们连接一条有向边。之后跑一边拓扑序,就可以得到最大值。建图稍微麻烦,用(i-1)+ k (k = 0,1,2)来分配一个块的一个状态id。
代码
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#include<bits/stdc++.h>
using namespace std;
const int N = 1e2 + 10;
int mp[N][N],d[N],dp[N],w[N];
struct Node{
int x,y,z;
}node[N];
int s[3][3] = {{1,1,0},{1,0,1},{0,1,1}};
int ans,n,kase=1;
void top_sort(){
n = (n-1)*3 + 2;
queue<int> qe;
for(int i=0;i<=n;i++) if(!d[i]) dp[i] = w[i],qe.push(i);
while(!qe.empty()){
int u = qe.front();qe.pop();
for(int i=0;i<=n;i++){
if(!mp[u][i]) continue;
dp[i] = max(dp[i],dp[u] + w[i]);
ans = max(dp[i],ans);
if(--d[i] == 0) qe.push(i);
}
}
}
signed main(){
IOS
while(cin>>n && n){
ans = 0;
memset(mp,0,sizeof(mp));
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++) cin>>node[i].x>>node[i].y>>node[i].z;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int p1=0;p1<3;p1++){
int x1,y1,z1,st = (i-1)*3+p1;
if(p1 == 0) x1 = node[i].x,y1 = node[i].y,z1 = node[i].z;
else if(p1 == 1) x1 = node[i].x,y1 = node[i].z,z1 = node[i].y;
else x1 = node[i].y,y1 = node[i].z,z1 = node[i].x;
w[st] = z1;
for(int p2=0;p2<3;p2++){
int x2,y2,z2,ed = (j-1)*3+p2;;
if(p2 == 0) x2 = node[j].x,y2 = node[j].y,z2 = node[j].z;
else if(p2 == 1) x2 = node[j].x,y2 = node[j].z,z2 = node[j].y;
else x2 = node[j].y,y2 = node[j].z,z2 = node[j].x;
w[ed] = z2;
if((x1 > x2 && y1 > y2) || (x1 > y2 && y1 > x2)) mp[st][ed] = 1,d[ed] ++;
}
}
top_sort();
cout<<"Case "<<kase++<<": maximum height = "<<ans<<endl;
}
return 0;
}
区间DP
石子合并
解释
状态表示 dp(i,j)表示合并 i ~ j 所花费的最小价值
目标状态 dp(1,n)
初始化 dp(i,i) 为0
转移方程 dp(i,j) dp(i,j) = min(dp(i,j),dp(i,k)+dp(k+1,j)) dp(i,j) += sum(i,j) 表示当前状态可以以k为分界点进行两堆的合并。
而dp(i,k) 也可以由更小的区间进行合并.
模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int dp[N][N],sum[N];
int main(){
int n; cin>>n;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++){
int a; cin>>a;
dp[i][i] = 0;
sum[i] = sum[i-1] + a;
}
for(int len=2;len<=n;len++)
for(int l=1;l<=n-len+1;l++){
int r = l + len - 1;
for(int k=l;k<r;k++) dp[l][r] = min(dp[l][k]+dp[k+1][r],dp[l][r]);
dp[l][r] += sum[r] - sum[l-1];
}
cout<<dp[1][n]<<endl;
return 0;
}
多边形
解释
环形拆分成链,变成两倍长度。然后与石子合并一样,可以由哪个点的两边合并。因为有乘法,所以要维护最小值,因为最小值乘最小值可以变成最大值。
状态表示 dp(i,j,0) 合并(i,j)区间所得到的最大值,dp(i,j,1) 合并(i,j) 区间得到的最小值.
目标状态 max{ dp(i,i+n-1,0) } 以i为起点的长度为n。
状态转移 当以k为分界点时
k+1表示加法时
dp(l,r,0) = max( dp ( l , r , 0 ) , dp ( l , k , 0 ) + dp ( k + 1 , r , 0 )
dp(l,r,1) = min( dp ( l , r , 1 ) , dp ( l , k , 1 ) + dp( k + 1 , r , 1 ) )
k+1表示乘法时
p,q 为 {0,1}
dp(l,r,0) = max( dp ( l , r , 0 ) , dp ( l , k , p ) * dp ( k + 1 , r , p )
dp(l,r,1) = min( dp ( l , r , 1 ) , dp ( l , k , q ) * dp( k + 1 , r , q ) )
模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1e2 + 10;
int dp[N][N][2];
int w[N];
char op[N];
int main(){
int n; cin>>n;
for(int i=1;i<=n;i++){
cin>>op[i]>>w[i];
op[n+i] = op[i];
dp[i][i][0] = dp[i][i][1] = w[i];
dp[i+n][i+n][0] = dp[i+n][i+n][1] = w[i];
}
for(int len=2;len<=n;len++)
for(int l=1;l<=2*n-len+1;l++){
int r = l + len - 1;
dp[l][r][0] = -1e9,dp[l][r][1] = 1e9;
for(int k=l;k<r;k++){
if(op[k+1] == 't'){
dp[l][r][0] = max(dp[l][k][0]+dp[k+1][r][0],dp[l][r][0]);
dp[l][r][1] = min(dp[l][k][1]+dp[k+1][r][1],dp[l][r][1]);
}else{
for(int p=0;p<=1;p++)
for(int q=0;q<=1;q++){
dp[l][r][0] = max(dp[l][k][p]*dp[k+1][r][q],dp[l][r][0]);
dp[l][r][1] = min(dp[l][k][p]*dp[k+1][r][q],dp[l][r][1]);
}
}
}
}
int ans = -1e9;
for(int i=1;i<=n;i++) ans = max(ans,dp[i][i+n-1][0]);
cout<<ans<<endl;
for(int i=1;i<=n;i++)
if(dp[i][i+n-1][0] == ans) cout<<i<<" ";
cout<<endl;
return 0;
}
更多例题
luogu能量项链 输入是存的是每一个节点的头节点,合并一个区间,枚举分界点k,表示 l ~ k 已经被合并且变成了l ~ k+1的值,k+1 ~ r 被合并且变成了k+1 ~ r+1的值。dp(l,r) = max(dp(l,k)+ dp(k+1,r)+ a(l)x a(k+1) * a(j+1),dp(l,r))。先拆分成链。代码
luogup3146 差不多板子,只不过合并的时候,必须两边都是相等的才能合并。代码
luogup4170 当只有一个颜色时,染色为1.当前s(l)== s(r) dp(l,r) = min(dp(l + 1,r),dp(l,r - 1)). 其它情况直接选取断点合并。代码
POJ2955 当s(l-1) 与 s(r-1)匹配时 dp(l,r) = dp(l+1,r-1)+ 2. 还可以直接通过两个区间合并得到。这两个就是题上给的两个情况 代码
HDU4632 与一般的区间dp稍微不同,由小区间合并而来,但区间与区间之间是固定合并,不用枚举分界点。当l == r的时候为1,当l != r时,若此时两端字符相同 dp(l,r)= (dp(l,r)+ dp(l+1,r-1)+ 1) % mod。一般情况下,dp(l,r) = (dp(l,r)+ dp(l+1,r)+ dp(l,r-1)- dp(l+1,r-1)+ mod )% mod 代码
luogup2858 卖的情况可以由小区间向大区间扩展,假设处理的区间为(l,r)那么对于这个区间,我们判断当前取左边的那个去卖还是取右边的那个去卖更划算。则为dp(l,r) = max(dp(l+1,r)+ a(l),dp(l,r-1)+ a(r))。但是因为已经处理好的区间按的是从天数1开始的,所以还要加上区间的和,即dp(l,r) = max(dp(l+1,r)+ a(l)+ sum(r)- sum(l),dp(l,r-1)+ a(r)+ sum(r-1)- sum(l-1)).
luogup3147 有点难想,可以设转移方程为dp(i,j)表示以j为起点,合并为i的最右边的值。那么转移为 如果dp(i-1,j)存在, dp(i,j)= dp(i-1,dp(i-1,j)+ 1),如果当前的值合法,存一下i,i的范围为1~58.因为所有数合并最多合并到18,最大值又为40. 代码
HDU2476 有点难想,先用一个空串全部转换为b,dp(l,r) = dp(l,r - 1) + 1. 枚举断点,当前满足p(k) == p(r) dp(l,r) = min(dp(l,k)+ dp(k+1,r-1),dp(l,r))。之后再将a转换为b,如果当前a串和b串的这位相同,则这一位可以不用被改变, 即 dp(1,k) = min(dp(1,i-1) + dp(i+1,k)… )[代码](String painter(HDU2476)(区间DP)_ddgo的博客-优快云博客)
树形DP
没有上司的舞会
解释
有强烈的自下向上推,想知道当前的人是否选择的最优价值,必须知道它儿子的最优价值。
状态表示 设dp(i,0)表示 第i个人不选的最大值,dp(i,1)为第i个人选的最大值。
目标状态 max(dp(i,0),dp(i,1)… )
转移方程
初始dp(i,1) = w(i)
dp(i,1)+= dp(j,0) j 属于 i的儿子节点
dp(i,0)+= max(dp(j,0),dp(j,1))
模板
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4 + 10;
int dp[N][2],w[N],d[N];
int h[N],e[N],ne[N],idx=1;
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u){
dp[u][1] = w[u];
for(int i=h[u];i;i=ne[i]){
int j = e[i];
dfs(j);
dp[u][1] += dp[j][0];
dp[u][0] += max(dp[j][1],dp[j][0]);
}
}
int main(){
int n; cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<n;i++){
int a,b; cin>>a>>b;
add(b,a); d[a] ++;
}
int root = 1;
while(d[root]) root ++;
dfs(root);
int ans = -1e9;
for(int i=1;i<=n;i++) ans= max(ans,max(dp[i][0],dp[i][1]));
cout<<ans<<endl;
return 0;
}
二叉苹果树
解释
树形DP + 分组背包
选择边的问题
普通分组背包,第一层枚举组,相当于枚举儿子节点dfs处理,第二层枚举体积,第三层枚举每一组里面每一个的物品。状态表示dp(i,j)。然后第一层可以优化掉,只需要体积逆序。
对于树形,有依赖,必须开一维存当前的根节点。即用dp(i,j)表示。
对于每一个儿子节点。都互不影响,可以看成不同的组,对于每一组,可以选择0~m个保留每一个的价值为dp(j,0…m)体积为 0…m
状态表示 dp(i,j)表示当前以i为节点,保留j条边所得到的最大值。
目标状态 dp(1,m)
状态转移 当前父节点保留i个时。对于当前的儿子节点,选j个时。
dp(u,i)= max(dp(u,i),dp(u,i-j-1)+ dp(v,j)+ w)
(i - j - 1) 减 1是,若当前选了子树,必须拿一个边出来连接。
模板
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int dp[N][N],n,m;
int e[N],w[N],ne[N],h[N],idx=1;
void add(int a,int b,int c){
w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa){
for(int i=h[u];i;i=ne[i]){
int j = e[i];
if(j == fa) continue;
dfs(j,u);
for(int x=m;x;x--)
for(int y=0;y<=x-1;y++)
dp[u][x] = max(dp[u][x],dp[u][x-y-1]+dp[j][y]+w[i]);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<n;i++){
int a,b,c; cin>>a>>b>>c;
add(a,b,c);add(b,a,c);
}
dfs(1,0);
cout<<dp[1][m]<<endl;
return 0;
}
选课
选择点的问题。
模板
#include<bits/stdc++.h>using namespace std;const int N = 1e3+10;int dp[N][N],n,m;int e[N],w[N],ne[N],h[N],idx=1;void add(int a,int b){ e[idx]=b,ne[idx]=h[a],h[a]=idx++;}void dfs(int u){ for(int i=h[u];i;i=ne[i]){ int j = e[i]; dfs(j); for(int x=m-1;x;x--) for(int y=1;y<=x;y++) dp[u][x] = max(dp[u][x],dp[u][x-y]+dp[j][y]); } for(int i=m;i;i--) dp[u][i] = dp[u][i-1] + w[u];}int main(){ cin>>n>>m; m += 1; for(int i=1;i<=n;i++){ int a; cin>>a>>w[i]; add(a,i); } dfs(0); cout<<dp[0][m]<<endl; return 0;}
更多列题
访问美术馆 先建立好一棵树,然后再进行DP.每一个节点可以看成完全背包问题。对每一个叶子节点先进行完全背包处理,然后父节点进行分组背包dp。代码
偷天换日 与上面那个题一样,只不过对于每一个叶子子节点,进行01背包处理。代码
Cell Phone Network G 设dp(i,0)表示i被自己覆盖,dp(i,1)表示i被它的儿子覆盖,dp(i,2)表示i被它的父亲覆盖 转移dp(i,0) += min(dp(j,0),dp(j,1),dp(j,2))
dp(i,2)+= min(dp(j,0),dp(j,1))。代码
dp(i,1)可以以一个儿子安装信号塔,其它的可以选择安,或者以它儿子来安。dp(i,1)= dp(k,0)+ ∑ j ∈ s o n , j ≠ k m i n ( d p [ j ] [ 0 ] , d p [ j ] [ 1 ] ) \sum_{j\in{son},j\ne{k}}min(dp[j][0],dp[j][1]) ∑j∈son,j=kmin(dp[j][0],dp[j][1])
接下来是如何寻找k,肯定为了让上面那团更小,假设当前有dp(x,0) + ∑ j ∈ s o n , j ≠ x m i n ( d p [ j ] [ 0 ] , d p [ j ] [ 1 ] ) \sum_{j\in{son},j\ne{x}}min(dp[j][0],dp[j][1]) ∑j∈son,j=xmin(dp[j][0],dp[j][1])
≤ \le ≤ dp(y,0) + ∑ j ∈ s o n , j ≠ y m i n ( d p [ j ] [ 0 ] , d p [ j ] [ 1 ] ) \sum_{j\in{son},j\ne{y}}min(dp[j][0],dp[j][1]) ∑j∈son,j=ymin(dp[j][0],dp[j][1]) 化简得到 dp(x,0)- min(dp(x,0),dp(x,1)) ≤ \le ≤ dp(y,0)- min(dp(y,0),dp(y,1))。
记录最小值后,dp(u,1) = dp(u,2) + mi
保安站岗 与上面那个一样,只不过上面那个节点的值默认为1,这个题节点有值。代码
树形DP之换根DP
积蓄程度
解释
先以任意度数不为1的任意节点为根节点(没有直接输出w(1)),求出d数组,表示以当前这个节点为父节点时,流向儿子节点的最大流量。
即可以得到d(u) += min(w(i),d(j)) 当前这个节点流量等于它到儿子节点最大的通量和儿子最大能接受的量。
当处理完后,再进行一次处理。设dp(i)表示以i为根节点时(源节点)最大流量。dp(root) = d(root)
然后根据父亲推出儿子节点的dp值。
假设当前父节点为u,子节点时j,dp(j) = d(j) + min(w(i),流向它父亲节点的流量)
流向它父亲的流量等于父节点流向除j之外的节点的流量
父节点流向除j之外的节点的流量 = dp(u)- min(d(j),w(i))
所以dp(j) = d(j) + min(w(i),dp(u)- min(d(j),w(i)))
对于儿子节度数为1,dp(j) = min(w(i),dp(u) - w(i))
模板
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);#include<bits/stdc++.h>using namespace std;const int N = 4e5+10;int dp[N],d[N],deg[N];int w[N],ne[N],h[N],e[N],idx=1;void add(int a,int b,int c){ w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;}void pre_dfs(int u,int fa){ for(int i=h[u];i;i=ne[i]){ int j = e[i]; if(j == fa) continue; pre_dfs(j,u); if(deg[j] == 1) d[u] += w[i]; else d[u] += min(d[j],w[i]); }}void dfs(int u,int fa){ for(int i=h[u];i;i=ne[i]){ int j = e[i]; if(j == fa) continue; if(deg[j] == 1) dp[j] = min(w[i],dp[u]-w[i]); else dp[j] = d[j] + min(w[i],dp[u]-min(d[j],w[i])); dfs(j,u); }}signed main(){ IOS int tt; cin>>tt; while(tt --){ idx = 1; int n; cin>>n; for(int i=1;i<=n;i++) dp[i] = d[i] = deg[i] = h[i] = 0; for(int i=1;i<n;i++){ int a,b,c; cin>>a>>b>>c; add(a,b,c);add(b,a,c); deg[a]++,deg[b]++; } int root = 1; while(deg[root] == 1 && root <= n) root ++; if(root > n){ cout<<w[1]<<endl; continue; } pre_dfs(root,0); dp[root] = d[root]; dfs(root,0); int ans = 0; for(int i=1;i<=n;i++) ans = max(ans,dp[i]); cout<<ans<<endl; } return 0;}
更多例题
HDU2196 预先维护根节点到儿子节点中的最大距离(d1)和次最大距离(d2)(要求为不同儿子)。
之后再处理,此时d1,d2表示以当前为根节点,得到的最大距离和次最大距离。进行转移,如父亲节点的dp(u)
为当前这个儿子节点转移过去,dp(i)为当前 max(d1(j),d2(u) + w(i))
然后维护d1,d2。
ICPC 2019-2020 North-Western Russia Regional Contest E. Equidistant
当合法的点到根节点的最大值和最小值相同时,该根节点才是一个正确的点。与上面那题类似,维护父亲节点到儿子节点的最大和最小值,次最大和次最小值。操作和上面一样。多维护最小的。
POJ3585 就是模板
luoguP2986 d(i)表示以当前为根节点,所有子树上的值到当前节点的距离。然后自上而下的进行转移。当前节点的值为dp(j) = dp(u)- w(i)x si(j)- dp(j) + si(u) - si(j)x w(i)+ dp(j)。si(j) = si(u)
概率DP
codeforces148D
解释
设dp(i,j) 为当前背包中有i个白鼠,j个黑鼠时,公主赢的概率。
初始化,当背包中只有白鼠时,赢的概率为1.
分情况:
1: 公主第一次取到白鼠。赢得概率为 i i + j \frac{i}{i+j} i+ji
2:公主取到黑鼠,龙取到黑鼠,跑出一只黑鼠。公主赢的概率只有在dp(i,j-3)的情况下发生,而当前情况发生的概率为s = j i + j − 1 ∗ j − 1 i + j − 1 ∗ j − 2 i + j − 2 \frac{j}{i+j-1}*\frac{j-1}{i+j-1}*\frac{j-2}{i+j-2} i+j−1j∗i+j−1j−1∗i+j−2j−2 ,所以赢的概率为 s × \times × dp(i,j-3)。
3:公主取到黑鼠,龙取到黑鼠,跑出一只白鼠。公主赢的概率只有在dp(i-1,j-2)的情况下发生,而当前情况发生的概率为s = j i + j − 1 ∗ j − 1 i + j − 1 ∗ i i + j − 2 \frac{j}{i+j-1}*\frac{j-1}{i+j-1}*\frac{i}{i+j-2} i+j−1j∗i+j−1j−1∗i+j−2i ,所以赢的概率为 s × \times × dp(i - 1,j - 2)。
代码
#include<iostream>#include<cstring>#include<algorithm>#include<iomanip>using namespace std;const int N = 1e3+10;double dp[N][N];signed main(){ int n,m; cin>>n>>m; for(int i=1;i<=n;i++) dp[i][0] = 1; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ double a = i,b = j; dp[i][j] += a/(a+b); if(j >= 3) dp[i][j] += b/(a+b)*(b-1)/(a+b-1)*(b-2)/(a+b-2)*dp[i][j-3]; if(i >= 1 && j >= 2) dp[i][j] += b/(a+b)*(b-1)/(a+b-1)*a/(a+b-2)*dp[i-1][j-2]; } cout<<fixed<<setprecision(9)<<dp[n][m]<<endl; return 0;}
更多例题
POJ3071 状态转移方程和相邻判断弄好就解决这题。状态dp(i , j)表示第i场j获胜的概率,怎么处理相邻,首先不让编号从1开始,从0开始,对于每一轮过后,每一个人的编号就会变成,id >>= 1,而一对相邻的数 a,b 且a是偶数,b是奇数,则a ^ 1 == b是必然的。 转移: dp(i,j) = dp(i-1,j) × \times × dp(i-1,k) × \times × p(j,k)。(建议scanf)
Codeforces Round #399 D 设dp(i,j)为第i天获得J种的概率,那么转移方程为dp(i,j) = dp(i-1,j) × \times × j k \frac{j}{k} kj+ dp(i-1,j-1) × \times × k − j + 1 k \frac{k-j+1}{k} kk−j+1 。处理好后,将dp(i,k)放到ans数组里面,每次二分查找即可。注,最坏天数不超过10000(不会证明,最笨方法直接代1000,1,1000),可以直接二维,也可以优化掉第一维 初始化注意。
POJ3744 矩阵快速幂优化的概率dp。设dp(i)表示走到i的概率,那么dp(i) = dp(i-1) × \times × p + dp(i-2) × \times × (1 - p). 由于很n会很大,仔细一看,和斐波那契矩阵快速幂板差不多。所以构造一个矩阵就可以求解。对每一段(每一个雷存放到a数组,排序,之后两个雷间的距离就是所求段),求出踩到雷的概率,那么没踩到雷的概率就为1减去就行了,每一段之积就是最终答案。注意细节
POJ2151 至少这一类的概率。
第一个条件: ∀ \forall ∀ 人至少通过1题 ⟺ \Longleftrightarrow ⟺ (1 - ∃ \exists ∃ 人未过题) 。
第二个条件: ∃ \exists ∃ 人通过N题 ⟺ \Longleftrightarrow ⟺ (1 - ∀ \forall ∀ 人都没有通过N题 )。
设dp(i,j)表示前i题通过了j题。 第一个条件就为 ∏ i = 1 t ( 1 − d p ( m , 0 ) ) \prod_{i=1}^{t}(1-dp(m,0)) ∏i=1t(1−dp(m,0)) 。
注意第一个条件与第二个条件的关系,设为a和b和c(每个人过0题的概率), a = b + 1 - b - c,条件1和条件2同时发生的概率即为b,b = a - (1 - b - c). (1 - b - c) = ∏ j = 1 t ∑ i = 1 n − 1 d p ( m , i ) \prod_{j=1}^{t}\sum_{i=1}^{n-1}dp(m,i) ∏j=1t∑i=1n−1dp(m,i) 。
期望DP
CollectingBugs
解释
设dp(i,j)表示找到i个bug,属于j个子系统到目标状态还需要的期望天数,则dp(n,s) = 0.
假设 当前找到的bug属于之前找到的bug ,也属于找到的子系统,那么dp(i,j) = dp(i,j) × \times × i n × j s \frac{i}{n}\times\frac{j}{s} ni×sj
同理 当前找到的bug不属于之前找到的bug,属于找到的子系统,那么dp(i,j) = dp(i+1,j) × \times × n − i n × j s \frac{n-i}{n}\times\frac{j}{s} nn−i×sj
当前找到的bug属于之前找到的bug,不属于找到的子系统,那么dp(i,j) = dp(i,j+1) × \times × i n × s − j s \frac{i}{n}\times\frac{s-j}{s} ni×ss−j
当前找到的bug不属于之前找到的bug,不属于找到的子系统,那么dp(i,j) = dp(i+1,j+1) × \times × n − i n × s − j s \frac{n-i}{n}\times\frac{s-j}{s} nn−i×ss−j
期望次数加1,因为无论哪种情况,天数都会加1.
dp(i,j)= dp(i,j) × \times × i n × j s \frac{i}{n}\times\frac{j}{s} ni×sj + dp(i+1,j) × \times × n − i n × j s \frac{n-i}{n}\times\frac{j}{s} nn−i×sj + dp(i,j+1) × \times × i n × s − j s \frac{i}{n}\times\frac{s-j}{s} ni×ss−j + dp(i+1,j+1) × \times × n − i n × s − j s \frac{n-i}{n}\times\frac{s-j}{s} nn−i×ss−j + 1.
⟺ \Longleftrightarrow ⟺ (dp(i+1,j) × \times × n − i n × j s \frac{n-i}{n}\times\frac{j}{s} nn−i×sj + dp(i,j+1) × \times × i n × s − j s \frac{i}{n}\times\frac{s-j}{s} ni×ss−j + dp(i+1,j+1) × \times × n − i n × s − j s \frac{n-i}{n}\times\frac{s-j}{s} nn−i×ss−j + 1)/ (1 - i n × j s \frac{i}{n}\times\frac{j}{s} ni×sj )。 注意除0得情况。
代码
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);#include<iostream>#include<iomanip>#define int long longusing namespace std;typedef long long ll;const int eps = 1e-7;const int N = 1e3+10;double dp[N][N];signed main(){ IOS int n,s; cin>>n>>s; int t = n*s; for(int i=n;i>=0;i--) for(int j=s;j>=0;j--){ if(i == n && j == s) continue; double p1 = 1.0*i*j/t,p2 = 1.0*(n-i)*j/t; double p3 = 1.0*i*(s-j)/t,p4 = 1.0*(n-i)*(s-j)/t; dp[i][j] = (p2*dp[i+1][j] + p3*dp[i][j+1] + p4*dp[i+1][j+1] + 1)/ (1-p1); } cout<<fixed<<setprecision(4)<<dp[0][0]<<endl; return 0;}
更多例题
HDU3853 和例题一样,也要注意除0得情况,这次加得期望是2(题上),用scanf输入。
HDU4405 设dp(i)表示已经到i到n还需要得目标扔骰子的期望次数,注意正推就不一样了。dp(n) = 0
如果当前这个点可以跳到后面,则dp(i) = dp(to(i))。
否则 dp(i) = (dp(i+1)+ … + dp(i+6))/ 6 + 1.
ZOJ3640 以能力值为转移关键,最多20000的大小.设dp(i)表示当前能力值为i到目标状态(不超过20000)时所需要的天数。当i大于c(j),那么dp(i) += t(j)/ n。否则为dp(i) += (dp(i+c(j))+ 1)/ n。
HDU4336 这题找到转移的点就很好写,把每个点选与不选作为转移点,状压就好写。设dp(x)表示当前状态为x,到目标状态还需要的次数,dp(11111…111) = 0。
dp(x) = dp(x | (1 << j)) × \times × p(j)… 这样,化简,把所有dp(x)放到一边( k1),其它的放一边( k2 )。那么dp(x) = (k2 + 1) / (1 - k1).
for(int j=0;j<n;j++){ if(((i >> j) & 1) == 1) k1 += p[j]; else k2 += dp[i | (1 << j)] * p[j];}
注意输出保留4位 (样例害人,哭)k1 里面还有一个都没选的情况加进来。
HDU4035 树形期望DP,推一个式子,化简,不好化简。
数位DP
windy数
解释
数位DP就是按这个数的每一位数来分析进行DP.从高位往低位走
题目可以转换为求1~l-1(a), 1~r(b) 的值,最后答案即为 b - a。
首先,对于给定范围,为了让所有的数不大于最右边的数,加一个限制,具体如下,假设最右边的数为b,当前枚举的数为a。再形成a这个数的过程中(因为按高位往低位形成),若上一位及其前面的位与b上对应的位都相同,那么当前这位枚举就有限制,限制在0 ~ b(pos),否则,那么限制打开,可以枚举0 ~ 9,注意前导零处理
之后可以用dfs求解dp(n,last)(n位,last(上一个),op(限制))因为当n,last,op相同时,得到的子问题是一样的解集。所以可以用记忆化搜索优化,注意记忆化一定要当op限制解除的时候再记忆化。具体再看博客
介绍。
对于某些题,前导0也会对记忆化产生影响。
模板
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#include<vector>using namespace std;const int N = 1e2 + 10;int dp[N][N];vector<int> s;int dfs(int n,int last,int op){ if(!n) return 1; if(!op && ~dp[n][last]) return dp[n][last]; int ma = op ? s[n] : 9; int res = 0; for(int i=0;i<=ma;i++){ if(abs(last - i) < 2) continue; if(last == 11 && i == 0) res += dfs(n-1,11,op & (i == ma)); else res += dfs(n-1,i,op & (i == ma)); } if(!op) dp[n][last] = res; return res;} int slove(int x){ memset(dp,-1,sizeof(dp)); s.clear(); s.push_back(-1); while(x){ s.push_back(x % 10); x /= 10; } return dfs((int)s.size()-1,11,1);}signed main(){ IOS int l,r; cin>>l>>r; cout<<slove(r) - slove(l-1)<<endl; return 0;}
更多例题
luogup2602 用dp(i,j)表示当前处理到第i位,有j个需要计算的数字,输出slove(r,0…9) - slove(l-1,0…9). dfs的参数为,pos当前的位置,lead前导零,limit限制,id当前需要统计的数字,sum当前需要统计数字的个数,当处理到最后时,return sum(以最后一次看,相当于最后的那个数字变化9次,每次都有sum个需要统计的数)。0的时候处理的较多,还需要lead限制解除才记忆。
luogup4317 转换为二级制,之后统计一下包含i个1的个数,i 为 1 ~ 50, 最后计算完之后,只需判断当前的1的个数与所需要的个数是否相同。统计完之后,快速幂成就行了。
luogup6218 如果理解了上面两个题,这个题就很简单了,维护1(b)的个数和0(a)的个数,最后处理完的时候返回a >= b.也需要注意前导零的时候。
luogup4999 与第一个例题基本一样,计算每一个数字出现的个数,记得取模。
luogup4127 设维护dp(i,sum,u),sum是每位之和,u是原数,因为u很大,可以对它取mod,取mod最好就是取sum,但是sum是变化的,所以可以直接固定它,最后当sum == mod(固定的模数才加1)注意sum为0的情况。
luogup3413 字符串读入大数,有性质,当某一位与前两位存在相同的,则满足题意,否则则不满足。参数需要存上一个,上上个,和前导零,限制,当前是否存在了子串ok,最后到底返回ok。
luogup4124 维护上个(a),上上个(b), 和4是否出现,8是否出现,k表示i == a 且 a == b ,注意 第一位从1开始,且当这个数小于1e10时返回0
acwing度的数量 无论是几进制,每一位只能是0或者1,记忆化存一下处理到当前位置已经有几个1,在记忆化搜索时,下次处理到当前位置,且还有那么多个1,就可以直接得到答案。其它的就是模板。代码
acwing数字游戏 每次存一下上一个取的值。 代码
acwing数字游戏 II 第二维状态存前面位之和取模,然后板子记忆化搜索。 代码
acwing不要62 第二维表示当前这一位的上一位,第三维为判断处理到这位是,是否还合法。 代码
状压DP
多用二进制表示状态,进行状态之间的转移。
蒙德里安的梦想
解释
以一行为边界,会发现,这行竖着放的下一行一定会被填,用二进制中的1表示当前行是竖着放的,
那么下一行的状态合法必须满足:
- 上一行与下一行的与为0,表示上一行竖着放的,下一行一定为0补全上一行,而上一行为0的,当前行可以为1也可以为0.
- 上一行与下一行的状态或上的状态,一定是连续的0的个数为偶数,因为与上之后,还为0的只有放横着的,横着的必须为偶数个。
每次先处理合法状态,存到数组里面。之后枚举当前行,当前的状态,再枚举上一行的状态,判断当前行是否可以由上一行转移过来。
代码
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);#include<bits/stdc++.h>#define int long longusing namespace std;const int N = 11;int vis[1<<N],dp[N+1][1<<N]; signed main(){ IOS int n,m; while(cin>>n>>m && n && m){ for(int i=0;i<(1 << m);i++){ int odd = 0,cnt = 0; for(int j=0;j<m;j++) if((i >> j) & 1) odd |= cnt,cnt = 0; else cnt ^= 1; vis[i] = (odd | cnt) ? 0 : 1; } dp[0][0] = 1; for(int i=1;i<=n;i++) for(int j=0;j<(1 << m);j++){ dp[i][j] = 0; for(int k=0;k<(1 << m);k++) if(!(j & k) && vis[j | k]) dp[i][j] += dp[i-1][k]; } cout<<dp[n][0]<<endl; } return 0;}
更多例题
WZB’sHarem 还是以行为突破口,先预处理出每一行的合法状态。即当前行的状态只有一个1,且这个1不与输入的1的位置冲突。之后与上述类似。代码
炮兵阵地 以行为突破口,先预处理每行的合法状态(每两个1之间必须大于等于两个0),然后将图中的每一行状态表示出来,用于判断这个状态是否能放置到当前行。转移必须与前两行有关。设 d p ( i , j , k ) dp(i,j,k) dp(i,j,k)表示当前行的状态为j,上一行的状态为k,那必须k与j == 0 并且 i与j == 0 并且 i与k == 0才能转移,注意空间,可以用滚动数组优化。代码