引入
我认为,状态机DP很常见,通常跟其他DP混合在一起;
一个显著特征就是,我们需要当前点的状态;
或者说,我们状态之间的转移,是与我们当前点的状态是有关系的;
假设当前点为 A 1 A_1 A1,那么只能从状态 B 1 B_1 B1转移
而不能从状态 B 2 B_2 B2转移;
例题
大盗阿福
题面
思路
f ( i , j ) f(i,j) f(i,j)表示前 i i i个店铺,当前状态为 j j j的所有抢法中的最大值;
如果当前店铺要抢,那么前一个必然不能抢;
即 f ( i , 1 ) = f ( i − 1 , 0 ) + a [ i ] f(i,1)=f(i-1,0)+a[i] f(i,1)=f(i−1,0)+a[i]
如果当前不抢,那么前一个抢不抢无所谓;
即 f ( i , 0 ) = m a x ( f ( i − 1 , 0 ) , f ( i − 1 , 1 ) ) f(i,0)=max(f(i-1,0),f(i-1,1)) f(i,0)=max(f(i−1,0),f(i−1,1))
Code
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int f[N][2];
int a[N];
void solve(){
int n;
cin >> n;
for(int i=1;i<=n;++i){
cin >> a[i];
f[i][0] = f[i][1] = 0;
}
f[0][0] = 0,f[0][1] = -1e9;//f(0,1)是非法状态,直接给个-INF
for(int i=1;i<=n;++i){
f[i][0] = max(f[i-1][0],f[i-1][1]);
f[i][1] = f[i-1][0] + a[i];
}
cout << max(f[n][0],f[n][1]) << '\n';
}
int main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int t;
cin >> t;
while(t--)
solve();
return 0;
}
股票买卖IV
题面
思路
f ( i , j , k ) f(i,j,k) f(i,j,k)表示前 i i i只股票,交易成功 j j j次,当前状态为 k k k的所有方案中的最大值;
其中 k = 0 k=0 k=0 表示手中没有股票, k = 1 k=1 k=1 表示手中有股票;
具体转移如下图
需要注意的是,买入加上卖出才算一次完整交易;
假设我们先不考虑交易次数,或者说现在允许我们进行无限次交易;
转移方程如下:
f ( i , 0 ) = m a x { f ( i − 1 , 0 ) , f ( i − 1 , 1 ) + a [ i ] } f(i,0) = max\{f(i-1,0),f(i-1,1)+a[i]\} f(i,0)=max{f(i−1,0),f(i−1,1)+a[i]};
f ( i , 1 ) = m a x { f ( i − 1 , 0 ) − a [ i ] , f ( i − 1 , 1 ) } f(i,1)=max\{f(i-1,0)-a[i],f(i-1,1)\} f(i,1)=max{f(i−1,0)−a[i],f(i−1,1)}
限制交易次数同理,只不过多一维度而已;
f [ i ] [ j ] [ 0 ] = { f [ i − 1 ] [ j ] [ 0 ] , f [ i − 1 ] [ j − 1 ] [ 1 ] + a [ i ] } m a x f[i][j][0] = \{f[i-1][j][0],f[i-1][j-1][1]+a[i]\}_{max} f[i][j][0]={f[i−1][j][0],f[i−1][j−1][1]+a[i]}max
f [ i ] [ j ] [ 1 ] = { f [ i − 1 ] [ j ] [ 1 ] , f [ i − 1 ] [ j ] [ 0 ] − a [ i ] } m a x f[i][j][1] = \{f[i-1][j][1],f[i-1][j][0]-a[i]\}_{max} f[i][j][1]={f[i−1][j][1],f[i−1][j][0]−a[i]}max
注意,买入是不算一次新完成的交易的;
而卖出是算一次新完成的交易的;
Code
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int f[N][110][2];
int a[N];
const int INF = 1e9;
void solve(){
int n,m;
cin >> n >> m;
for(int i=1;i<=n;++i) cin >> a[i];
//初始化
for(int i=0;i<=n;++i)
for(int j=0;j<=m;++j){
if(j == 0) f[i][j][0] = 0,f[i][j][1] = -INF;
else f[i][j][0] = f[i][j][1] = -INF;
}
for(int i=1;i<=n;++i){
for(int j=0;j<=m;++j){
f[i][j][0] = f[i-1][j][0];
if(j>0) f[i][j][0] = max(f[i][j][0],f[i-1][j-1][1]+a[i]);
f[i][j][1] = max(f[i-1][j][1],f[i-1][j][0]-a[i]);
}
}
int ans = 0;
//至多m次,具体几次不知道
for(int j=0;j<=m;++j){
//最后手里不持有肯定比持有赚
ans = max(ans,f[n][j][0]);
}
cout << ans << '\n';
}
int main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
股票买卖V
题面
思路
这道题其实比上一题简单,只不过多加了一个状态;
f ( i , k ) f(i,k) f(i,k)表示前 i i i天,当前状态为 k k k的所有买法中的最大值;
k
=
0
k=0
k=0 表示当前手上没有股票,不允许买入;
k
=
1
k=1
k=1 表示手上有股票
k
=
2
k=2
k=2 表示手上没有股票,允许买入;
状态转移方程来源请看上方自动机的图片;
f
[
i
]
[
1
]
=
m
a
x
(
f
[
i
−
1
]
[
1
]
,
f
[
i
−
1
]
[
2
]
−
a
[
i
]
)
;
f[i][1] = max(f[i-1][1],f[i-1][2] - a[i]);
f[i][1]=max(f[i−1][1],f[i−1][2]−a[i]);
f
[
i
]
[
2
]
=
m
a
x
(
f
[
i
−
1
]
[
0
]
,
f
[
i
−
1
]
[
2
]
)
;
f[i][2] = max(f[i-1][0],f[i-1][2]);
f[i][2]=max(f[i−1][0],f[i−1][2]);
f
[
i
]
[
0
]
=
m
a
x
(
f
[
i
]
[
0
]
,
f
[
i
−
1
]
[
1
]
+
a
[
i
]
)
;
f[i][0] = max(f[i][0],f[i-1][1] + a[i]);
f[i][0]=max(f[i][0],f[i−1][1]+a[i]);
Code
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
//0表示手上没股票的第一天
//1表示手上有股票
//2表示手上没股票的第二天及以上
int f[N][3];
int a[N];
const int INF = 1e9;
void solve(){
int n;
cin >> n;
for(int i=1;i<=n;++i) cin >> a[i];
//初始化
for(int i=0;i<=n;++i)
f[i][2] = f[i][0] = f[i][1] = -INF;
f[0][2] = 0;
for(int i=1;i<=n;++i){
f[i][1] = max(f[i-1][1],f[i-1][2] - a[i]);
f[i][2] = max(f[i-1][0],f[i-1][2]);
f[i][0] = max(f[i][0],f[i-1][1] + a[i]);
}
cout << max(f[n][0],f[n][2]) << '\n';
}
int main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
设计密码
题面
思路
f ( i , j ) f(i,j) f(i,j)表示在主串中匹配到第 i i i位(还没匹配成功,扫到 i + 1 i+1 i+1时就匹配成功了),在子串的KMP数组位于第 j j j位;
这个 j j j也可以解释成和子串 T T T匹配到第 j j j位;
在 K M P KMP KMP算法中,如果子串的长度为 m m m,那么走到 m m m就算匹配成功了;
我们这里想要匹配失败,那么我们就不能匹配到第 m m m位;
状态转移的话,我们可以考虑对于每一个 < i , j > <i,j> <i,j>对,枚举 i i i的所有可能取值;
假设当前主串的第 i i i个字符的取值为 c h ch ch;
这个 c h ch ch会跳到 K M P KMP KMP数组的第 u u u位;
那么有 f ( i , u ) + = f ( i − 1 , j ) f(i,u) += f(i-1,j) f(i,u)+=f(i−1,j)
即 f ( i − 1 , j ) f(i-1,j) f(i−1,j)是可以转移到 f ( i , u ) f(i,u) f(i,u)的,是有贡献的;
Code
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e2 + 10;
const int MOD = 1e9 + 7;
int n,m,nex[N];
ll f[N][N];//f(i,j)表示当前匹配到第i位(还未匹配成功),在子串的KMP数组中的位置是j
char str[N];
void solve(){
cin >> n;
cin >> (str+1);
m = strlen(str+1);
for(int i=2,j=0;i<=m;++i){
while(j && str[i] != str[j+1]) j = nex[j];
if(str[i] == str[j+1]) ++j;
nex[i] = j;
}
f[0][0] = 1;
for(int i=1;i<=n;++i){
//如果匹配到m 就说明包含子串T了
for(int j=0;j<m;++j){
for(int k=0;k<26;++k){
char ch = 'a' + k;
int u = j;
while(u && ch != str[u+1]) u = nex[u];
if(ch == str[u+1]) ++u;
if(u<m){
f[i][u] += f[i-1][j];
f[i][u] %= MOD;
}
}
}
}
ll ans = 0;
for(int j=0;j<m;++j){
ans += f[n][j];
ans %= MOD;
}
cout << ans << '\n';
}
int main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
优化
我们可以预处理每个 k k k是怎么转移的,这样就可以直接转移;
而不用暴力的去跑 K M P KMP KMP了;
_ n e x [ j ] [ k ] \_nex[j][k] _nex[j][k]表示匹配到子串的第 j j j,如果主串是 k + ′ a ′ k+'a' k+′a′, K M P KMP KMP后的值是多少;
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e2 + 10;
const int MOD = 1e9 + 7;
int n,m,nex[N],_nex[N][N];
ll f[N][N];//f(i,j)表示当前匹配到第i位(还未匹配成功),在子串的KMP数组中的位置是j
char str[N];
void solve(){
cin >> n;
cin >> (str+1);
m = strlen(str+1);
for(int i=2,j=0;i<=m;++i){
while(j && str[i] != str[j+1]) j = nex[j];
if(str[i] == str[j+1]) ++j;
nex[i] = j;
}
for(int j=0;j<m;++j){
for(int k=0;k<26;++k){
char ch = k + 'a';
int u = j;
while(u && ch != str[u+1]) u = nex[u];
if(ch == str[u+1]) ++u;
_nex[j][k] = u;
}
}
f[0][0] = 1;
for(int i=1;i<=n;++i){
//如果匹配到m 就说明包含子串T了
for(int j=0;j<m;++j){
for(int k=0;k<26;++k){
char ch = 'a' + k;
int u = _nex[j][k];
if(u < m){
f[i][u] += f[i-1][j];
f[i][u] %= MOD;
}
}
}
}
ll ans = 0;
for(int j=0;j<m;++j){
ans += f[n][j];
ans %= MOD;
}
cout << ans << '\n';
}
int main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
与其他DP混合的情况
E2. Rubik’s Cube Coloring (hard version)
遇到再补…