dp及其它优化

“纵使天地渺远,空间永远不够。”
“即便今昔深邃,时间不可窥探。”

概率期望 dp

期望 x 概率 x 动态规划

概率的定义:

对于事件 $A$ ,在进行了多次试验时候,其发生的次数与总实验次数之比 $\frac{N_A}{n}$ 会逐渐稳定在某一数值 $p$ 的附近,则 $p$ 被称为 $A$ 在该条件下发生的概率,记为 $P(A)=p$ 。

期望的定义:

对于一个事件 $A$ ,如果 $A$ 的结果有 $k$ 种,每一种结果的权值为 $a_i$ ,概率为 $p_i$ ,则 $A$ 事件的期望记为 $E(A)$ ,则有 $E(A)=\sum\limits_{i=1}^{k} a_ip_i$ 。

概率 dp

采用顺推法,从初始状态(一般来说是 $dp_{0,0..0}$ )到所有的限制,同一般的 DP 类似的,难点依然是对状态转移方程的刻画,只是这类题目经过了概率论知识的包装。

例题 CF148D Bag of Mice

参考资料

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
using namespace std;
const int MAXN=1e3+1;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
int W,B;
double dp[MAXN][MAXN];
int main()
{
// freopen("dp-pro.in","r",stdin);
// freopen("dp-pro.out","w",stdout);
read(W,B);
for(int i=1;i<=W;++i) dp[i][0]=1.0;
for(int i=1;i<=B;++i) dp[0][i]=0;
for(int i=1;i<=W;++i)
for(int j=1;j<=B;++j)
{
double ix=i,jx=j;
dp[i][j]+=(double)(ix/(ix+jx));
if(j>=3) dp[i][j]+=double(jx/(ix+jx))*double((jx-1)/(ix+jx-1))
*double((jx-2)/(ix+jx-2))*dp[i][j-3];
if(i>=1&&j>=2) dp[i][j]+=double(jx/(ix+jx))*dp[i-1][j-2]
*double((jx-1)/(ix+jx-1))*double(ix/(ix+jx-2));
}
printf("%.9lf",dp[W][B]);
return 0;
}
/*
5 5
*/

期望 dp

正难则反,这是亘古不变的真理。

设有事件 $A$ ,则有 $dp[x]$ 表示在 $x$ 次实现之后 $A$ 满足某种条件 $T$ 的期望。一般而言,如果有 $dp[M]=1$ ,则采用逆序推理,否则有 $dp[0]=0$ ,然后顺序推理。

接下来 $4$ 道题,都是对期望 $\text{dp}$ 的讲解。


例题

FAVDICE

用 $dp[x]$ 表示当前已经投掷了 $x$ 面,投到 $n$ 面的期望次数。

对于 $dp[n]$ ,因为我们已经投掷了 $n$ 面了,所以不需要再投就可以满足条件,所以有 $dp[n]=0$ 。

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
using namespace std;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
int T,N;
double dp[1001];
int main()
{
// freopen("dp-probability.in","r",stdin);
// freopen("dp-probability.out","w",stdout);
read(T);
while(T--)
{
read(N);
dp[N]=0;
for(int i=N-1;i>=0;--i)
dp[i]=dp[i+1]+1.0*N/(N-i);
printf("%.2lf\n",dp[0]);
}
return 0;
}
/*
2
1
12
*/

换教室

能够被称为是期望 dp 的模板题,虽然我没有这么觉得

因为 $v\le 300$ ,则保证了我们求出全源最短路的复杂度,使用 Floyd 的 $\mathcal O(n^3)$ 求出并在 $\mathcal O(1)$ 内查询。

然后其实就会变成一个标准的概率期望的动态规划问题:

这应该是我写过最复杂的式子了。

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
using namespace std;
const int MAXN=2e3+1;
const int MAXM=2e3+1;
const int MAXV=3e2+1;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
int N,M,V,E;
int Now[MAXN],Oth[MAXN];
double p[MAXN],dp[MAXN][MAXM][3];
int Dist[MAXV][MAXV];
inline void Floyd()
{
for(int k=1;k<=V;++k)
for(int i=1;i<=V;++i)
for(int j=1;j<=V;++j)
Dist[i][j]=min(Dist[i][j],Dist[i][k]+Dist[k][j]);
}
int main()
{
// freopen("dp-prob.in","r",stdin);
// freopen("dp-prob.out","w",stdout);
memset(Dist,0x3f,sizeof(Dist));
read(N,M,V,E);
for(re int i=1;i<=N;++i) read(Now[i]);
for(re int i=1;i<=N;++i) read(Oth[i]);
for(re int i=1;i<=N;++i) scanf("%lf",&p[i]);
for(int i=1,u,v,w;i<=E;++i)
{
read(u,v,w);
Dist[u][v]=Dist[v][u]=min(Dist[u][v],w);
}
Floyd();
for(int i=1;i<=V;++i) Dist[i][i]=Dist[i][0]=Dist[0][i]=0;
for(int i=1;i<=N;++i)
for(int j=0;j<=M;++j)
for(int k=0;k<=1;++k)
dp[i][j][k]=1e9;
dp[1][0][0]=dp[0][0][0]=0;
for(int i=1;i<=N;++i)
for(int j=0;j<=M;++j)
{
dp[i][j][0]=min(dp[i][j][0],min(dp[i-1][j][0]+Dist[Now[i-1]][Now[i]],dp[i-1][j][1]+Dist[Now[i-1]][Now[i]]*(1.0-p[i-1])+Dist[Oth[i-1]][Now[i]]*p[i-1]));
if(j>0) dp[i][j][1]=min(dp[i][j][1],min(dp[i-1][j-1][0]+Dist[Now[i-1]][Now[i]]*(1.0-p[i])+Dist[Now[i-1]][Oth[i]]*p[i],dp[i-1][j-1][1]+Dist[Oth[i-1]][Oth[i]]*p[i]*p[i-1]+Dist[Oth[i-1]][Now[i]]*p[i-1]*(1.0-p[i])+Dist[Now[i-1]][Oth[i]]*(1.0-p[i-1])*p[i]+Dist[Now[i-1]][Now[i]]*(1.0-p[i-1])*(1.0-p[i])));
}
double ans=1e9;
for(int i=0;i<=M;++i) ans=min(ans,min(dp[N][i][0],dp[N][i][1]));
/*for(int i=1;i<=N;++i)
{
for(int j=0;j<=M;++j) printf("%.2lf ",dp[i][j]);
puts("");
}*/
printf("%.2lf",ans);
return 0;
}
/*
3 2 3 3
2 1 2
1 2 1
0.8 0.2 0.5
1 2 5
1 3 3
2 3 1
*/

Tribles

有一个很贴切的事例:如果一个人被车创死的概率为 $p$ ,则两个人同时被车创死的概率就应该是 $p^2$ ,这是理所当然的。

所以,我们可以忽视条件 $k$ ,把 $k$ 只生物当做 $1$ 只来对待,然后转化成常规的概率 dp 即可。

对应有 $n$ 种情况,用 $dp[i]$ 表示第 $i$ 天死亡的概率,则有:

$dp[i]=\sum dp[i-1]^j\times p[j]$ ,其中 $p[j]$ 是这只生物变成 $j$ 只的概率。

而答案就是 $dp[M]^k$ 即可。

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
using namespace std;
const int MAXN=1e3+1;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
int Test,N,K,M;
double p[MAXN],dp[MAXN];
int main()
{
// freopen("dp-pro.in","r",stdin);
// freopen("dp-pro.out","w",stdout);
read(Test);
for(int Case=1;Case<=Test;++Case)
{
memset(dp,0,sizeof(dp));
memset(p,0,sizeof(p));
read(N,K,M);
for(int i=0;i<N;++i) scanf("%lf",&p[i]);
for(int i=1;i<=M;++i)
for(int j=0;j<N;++j)
dp[i]+=p[j]*pow(dp[i-1],j);
printf("Case #%d: %.7lf\n",Case,pow(dp[M],K));
}
return 0;
}
/*
4
3 1 1
0.33
0.34
0.33
3 1 2
0.33
0.34
0.33
3 1 2
0.5
0.0
0.5
4 2 2
0.5
0.0
0.0
0.5
*/

奖励关


矩阵加速 dp

字面意思,使用矩阵快速幂来优化动态规划已达到时间复杂度的限制。

例题

分块

对于 $60\% $ 的做法:考虑首先求出 $a$ 与 $b$ 的所有重复元素,然后进行背包:

$f[i]=\sum f[i-j],j\ \text{合法}$

这样的复杂度大概是 $\mathcal O(100n)$ ,勉强能过。

但是对于 $n\le 10^{18}$ ,那真是异想天开。

考虑加速。无论如何,有 $0\le x\le 100$ ,则 $f[i]$ 最多是从 $f[i-100]\sim f[i-1]$ 转移而来的,那么考虑一个 $1\times Len$ 的矩阵($len$ 表示最多能够转移的个数),然后考虑 $01$ 矩阵。使得:

计算出转移矩阵应该是:

img

即可。

答案矩阵的第 $1$ 行第 $1$ 列就是最终答案,时间复杂度 $\mathcal O(Len^3\log n)$

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
using namespace std;
const int MAXL=101;
const int Mod=1e9+7;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
ll Length;
int N,M;
int Len,Num[MAXL],dp[MAXL];
bool a[MAXL],b[MAXL];
struct Matrix
{
ll rix[MAXL][MAXL];
inline void clear()
{
memset(rix,0,sizeof(rix));
}
const Matrix operator*(Matrix b)const
{
Matrix c;
c.clear();
for(int i=1;i<=Len;++i)
for(int j=1;j<=Len;++j)
for(int l=1;l<=Len;++l)
c.rix[i][j]=(c.rix[i][j]+rix[i][l]*b.rix[l][j])%Mod;
return c;
}
}Zero,Ans;
inline Matrix qPow(Matrix s,ll b)
{
Matrix res;
res.clear();
for(int i=1;i<=Len;++i) res.rix[i][i]=1;
while(b)
{
if(b&1) res=res*s;
s=s*s;
b>>=1;
}
return res;
}
int main()
{
// freopen("dp-matrix.in","r",stdin);
// freopen("dp-matrix.out","w",stdout);
read(Length);
read(N);
for(int i=1,x;i<=N;++i) read(x),a[x]=1;
read(M);
for(int i=1,x;i<=M;++i) read(x),b[x]=1;
for(int i=1;i<=100;++i) if(a[i]&b[i]) Zero.rix[1][i]=1,Len=i;
for(int i=1;i<Len;++i) Zero.rix[i+1][i]=1;
dp[0]=1;
for(int i=1;i<=Len;++i)
for(int j=1;j<=i;++j)
if(a[j]&&b[j]) dp[i]=(dp[i]+dp[i-j])%Mod;
for(int i=1;i<=Len;++i) Ans.rix[i][1]=dp[Len-i];
Ans=qPow(Zero,Length-Len+1)*Ans;
printf("%lld",Ans.rix[1][1]);
return 0;
}
/*
19260817
7
8 9 6 3 7 2 1
7
4 5 2 9 7 8 3
*/

倍增 dp

例题

P1613 跑路

这道题是最短路径和倍增思想的结合题。

首先说一下一个明显的错误: 绝对不能直接求最短路

原因很简单,题目中有说,有跑路器。

那么进一步思考,发现,任意 $2^k$ 的路径长度都可以在 $1$ 秒内走完,因此如果有 $u,v$ 两点间的最短距离为 $2^k$ 即可认为这两点之间有一条边(本题中边权全部为 $1$ )。

接下来,思路就很简单了。令 $f_{i,j,k}$ 的意义为能否在 $2^i$ 时间内从点 $j$ 走到点 $k$ ,可以为 $1$ ,不可以为 $0$。进一步,由倍增的思想可以得到:若 $f_{i-1, v, k}$ 和 $f_{i-1, k, u}$ 同时为 $1$ ,则 $f_{i, v, u}$ 为 $1$ 。

在这些都预处理完成之后,在跑一个最短路,就正确了。

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define underMax(x,y) ((x)>(y)?(x):(y))
#define underMin(x,y) ((x)<(y)?(x):(y))
typedef long long LL;
using namespace std;
const int MAXN=101;
template<class T>
inline void underRead(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=gh();
if(t) x=-x;
}
bool G[MAXN][MAXN][MAXN];
int Dist[MAXN][MAXN],N,M,Qx,Qy;
int main()
{
// freopen("run.in","r",stdin);
// freopen("run.out","w",stdout);
underRead(N),underRead(M);
memset(Dist,0x3f,sizeof(Dist));
memset(G,0,sizeof(G));
for(int i=1;i<=M;++i)
{
underRead(Qx),underRead(Qy);
Dist[Qx][Qy]=1;G[Qx][Qy][0]=1;
}
for(int k=1;k<=64;++k)
{
for(int i=1;i<=N;++i)
{
for(int j=1;j<=N;++j)
{
for(int l=1;l<=N;++l)
{
if(G[i][j][k-1]&&G[j][l][k-1])
{
G[i][l][k]=1;
Dist[i][l]=1;
}
}
}
}
}
for(int k=1;k<=N;++k)
{
for(int i=1;i<=N;++i)
{
for(int j=1;j<=N;++j)
{
Dist[i][j]=underMin(Dist[i][j],Dist[i][k]+Dist[k][j]);
}
}
}
printf("%d",Dist[1][N]);
return 0;
}
/*
4 4
1 1
1 2
2 3
3 4
*/

P1081 [NOIP2012 提高组] 开车旅行


数据结构优化 dp

线段树优化

这东西应该是最常见的数据结构优化(还有树状数组)了,将 $\mathcal O(n^2)$ 的 $\text{Dp}$ 转移优化到 $\mathcal O(n\log n)$ 。

对于一种转移:$Dp[i]=\max/\min(Dp[j]+calc(i,j)),1\leq j\leq i$ 的这种,暴力转移可以做到 $\mathcal O(n^2)$ ,但是我们可以用线段树来统计区间的最大的 $j$ 并转移,这样的复杂度查询只有 $\mathcal O(\log n)$ 。那么,此类的操作分为三步:查询,修改,上传。