Dynamic Programming
动态规划是一种穷举算法,通常基于一个递推公式和一个或多个初始状态。当前问题的解可以分解为多个子问题解得出。使用动态规划只需要多项式时间复杂度,因为比回溯法和暴力法快很多,体现了以空间换时间的算法思想
适用动态规划问题的特点:1.最优子结构,将母问题分解为子问题后,当子问题最优时,母问题通过优化选择一定最优的情况(或者说成母问题的最优解可由子问题的最优解构建得到)2.重复子序列,不同的决策序列,到达某个相同的阶段时,可能会产生相同的状态。3.无后效性,子问题的解一旦确定,就不再改变,不受它之后包含它的更大问题的求解决策影响
经典使用条件:dfs暴力计算无法满足程序特定时间对数据量的处理需求时,用动态规划以时间换空间
分类有:线性dp,区间dp,背包dp,树形dp,状态压缩dp,数位dp,计数型dp,递推型dp,概率型dp,博弈型dp,记忆化搜索
使用动态规划时,必须掌握的一个技巧是滚动数组优化,这种方法可以在不损失时间的情况下换取空间,方法是除去第一项并仔细斟酌顺序,注重动态规划状态的0/1态的分类判断;另外一个技巧是使用动态规划时候先列表格找规律;必须注意初始状态的定义
线性dp
单串
最长上升子序列(LIS,Longest Increasing Subsequence)
//O(n^2)的DP法
#include<bits/stdc++.h>
using namespace std;
int a[6000],dp[6000];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
dp[i]=1;
cin>>a[i];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
}
int maxn=0;
for(int i=1;i<=n;i++)
{
//cout<<dp[i]<<endl;
maxn=max(maxn,dp[i]);
}
cout<<maxn;
return 0;
}
最长有效括号
#include<bits/stdc++.h>
using namespace std;
string s;
int main()
{
cin>>s;
int n=s.length();
int dp[30010];
for(int i=0;i<n;i++)
{
dp[i]=0;
}
for(int i=0;i<n;i++)
{
if(s[i]=='(')
{
dp[i]=0;
}
else
{
if(dp[i-1]=='(')
{
dp[i]=dp[i-2]+2;
}
else
{
if(s[i-1-dp[i-1]]=='(')
{
dp[i]=dp[i-1]+2+dp[i-2-dp[i-1]];
}
}
}
}
int maxn=0;
for(int i=0;i<n;i++)
{
maxn=max(maxn,dp[i]);
}
cout<<maxn;
return 0;
}
摆动序列
通过列表法,观察前几项数据的共同特点,从而做出结论,列表法数据表项尽量要列得细,不要害怕去枚举举例计算(实在不行可以尝试计算机暴力出前几个解,再找规律)
最大整除子集
基本思路与LIS一样,不同的是有先排序的操作,并且要输出它的最大路线之一(根据答案从后往前输,其实也可以建一个辅助数组,存储答案路径的前驱结点)
class Solution {
public:
vector<int> largestDivisibleSubset(vector<int>& nums) {
int len = nums.size();
// 排序,目的使整除关系有序
sort(nums.begin(), nums.end());
vector<int> dp(len, 1);
// 定义最大长度和最大值
int maxLen = 1;
int maxVal = dp[0];
// 窗口从第二个开始,即包括1,2两个位置的元素开始
for(int i = 1; i < len; i ++) {
for(int j = 0 ; j < i; j ++) {
// 更新dp[i]
if(nums[i] % nums[j] == 0){
dp[i] = max(dp[i], dp[j] + 1);
}
}
// 更新最大长度和最大值
if(dp[i] > maxLen) {
maxLen = dp[i];
maxVal = nums[i];
}
}
// 定义返回的数组
vector<int> res;
if(maxLen == 1) {
res.push_back(nums[0]);
return res;
}
// 倒序遍历
for(int i = len - 1; i >= 0 && maxLen > 0; i --) {
if(dp[i] == maxLen && maxVal % nums[i] == 0) {
res.push_back(nums[i]);
maxLen --;
maxVal = nums[i];
}
}
return res;
}
};
分割数组的最大值
其实可以用二分来做,因为它特有的不减性质(可以相等的单调),用dp方法时状态转移方程式较难
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
]
,
m
a
x
(
d
p
[
k
]
[
j
−
1
]
,
s
u
b
[
i
]
−
s
u
b
[
k
]
)
)
dp[i][j] = min(dp[i][j], max(dp[k][j - 1], sub[i] - sub[k]))
dp[i][j]=min(dp[i][j],max(dp[k][j−1],sub[i]−sub[k]))
思路是遍历k为倒数第一个数组的开始,使得由小到大的状态转移成立
乘积最大子数组
int maxProduct(vector<int>& nums) {
int cur_max;
vector<int> max_sub_arr(nums.size(),0),min_sub_arr(nums.size(),0);
max_sub_arr[0]=nums[0];
min_sub_arr[0]=nums[0];
cur_max=max_sub_arr[0];
for(int i=1;i<nums.size();i++)
{
max_sub_arr[i]=max(max_sub_arr[i-1]*nums[i],min_sub_arr[i-1]*nums[i]);
max_sub_arr[i]=max(max_sub_arr[i],nums[i]);
min_sub_arr[i]=min(max_sub_arr[i-1]*nums[i],min_sub_arr[i-1]*nums[i]);
min_sub_arr[i]=min(min_sub_arr[i],nums[i]);
cur_max=cur_max>max_sub_arr[i]?cur_max:max_sub_arr[i];
}
return cur_max;
}
俄罗斯套娃信封问题
和LIS的想法一模一样
双串
最长公共子序列 (LCS)
根本思路是递推,二维的动态规划(经典升维,dp做不了怎么办,那就再加一维)
#include<bits/stdc++.h>
using namespace std;
const int N=5e3;
int n,a[N],b[N],LCS[N][N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<=n;i++)
{
cin>>b[i];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(a[i]==b[j]) LCS[i][j]=LCS[i-1][j-1]+1;
else LCS[i][j]=max(LCS[i-1][j],LCS[i][j-1]);
}
}
cout<<LCS[n][n];
return 0;
}
交叉字符串
把一二串的个数用二维标记,递推求解
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int n1 = s1.size();
int n2 = s2.size();
if ( n1 + n2 != s3.size() ) return false;
vector<vector<bool>> f( n1 + 1, vector<bool>(n2 + 1) );
f[0][0] = true;
for ( int i = 0; i <= n1; ++i ) {
for (int j = 0; j <= n2; ++j ) {
if ( i && s3[i + j - 1] == s1[i - 1] ) {
f[i][j] = f[i - 1][j];
}
if ( j && s3[i + j - 1] == s2[j - 1] )
f[i][j] = f[i][j] | f[i][j - 1];
}
}
return f[n1][n2];
}
};
不同的子序列
转移方程
$$
如果s[i-1] == t[j-1]:则dp[i][j] = dp[i-1][j-1] + dp[i][j-1]\
如果s[i-1] != t[j-1]:则dp[i][j] = dp[i][j-1]
$$
如果相同:相同的数量+不相同的数量;如果不相同:不相同的数量
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp =[[0] * (len(s)+1) for _ in range(len(t)+1)]
for _ in range(len(s)+1):
dp[0][_] = 1
for i in range(1,len(t)+1):
for j in range(1,len(s)+1):
if s[j-1] == t[i-1]:
dp[i][j] = dp[i-1][j-1] + dp[i][j-1]
else:
dp[i][j] = dp[i][j-1]
return dp[i][j]
两个字符串的删除操作
最长公共子序列(LCS)的衍生,相同的思想
编辑距离
t e x t 1 [ i ] = t e x t 2 [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] t e x t 1 [ i ] ! = t e x t 2 [ j ] d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j − 1 ] ) text1[i]=text2[j]\quad dp[i][j]=dp[i−1][j−1]\\text1[i]!=text2[j]\quad dp[i][j]=min(dp[i−1][j],dp[i][j−1],dp[i−1][j−1]) text1[i]=text2[j]dp[i][j]=dp[i−1][j−1]text1[i]!=text2[j]dp[i][j]=min(dp[i−1][j],dp[i][j−1],dp[i−1][j−1])
通识符匹配
s [ i ] = = p [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = ? d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = ∗ d p [ i ] [ j ] = d p [ i ] [ j − 1 ] ∣ ∣ d p [ i − 1 ] [ j ] s[i]==p[j]\quad dp[i][j]=dp[i−1][j−1]\\p[j]==?\quad dp[i][j]=dp[i−1][j−1]\\p[j]==∗\quad dp[i][j]=dp[i][j−1]∣∣dp[i−1][j] s[i]==p[j]dp[i][j]=dp[i−1][j−1]p[j]==?dp[i][j]=dp[i−1][j−1]p[j]==∗dp[i][j]=dp[i][j−1]∣∣dp[i−1][j]
正则表达式匹配
s [ i ] = = p [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = . d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = ∗ d p [ i ] [ j ] = d p [ i ] [ j − 1 ] p [ j ] = = ∗ & & ( s [ i ] = = p [ j − 1 ] ∣ ∣ p [ j − 1 ] = = . ) d p [ i ] [ j ] = d p [ i − 1 ] [ j ] s[i]==p[j]\quad dp[i][j]=dp[i−1][j−1]\\p[j]==.\quad dp[i][j]=dp[i−1][j−1]\\p[j]==∗\quad dp[i][j]=dp[i][j−1]\\p[j]==∗ \&\& (s[i]==p[j−1] ∣∣ p[j−1]==.)\quad dp[i][j]=dp[i−1][j] s[i]==p[j]dp[i][j]=dp[i−1][j−1]p[j]==.dp[i][j]=dp[i−1][j−1]p[j]==∗dp[i][j]=dp[i][j−1]p[j]==∗&&(s[i]==p[j−1]∣∣p[j−1]==.)dp[i][j]=dp[i−1][j]
经典问题
最大子序和
n u m s [ i ] = m a x ( n u m s [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) nums[i] = max(nums[i-1]+nums[i], nums[i]) nums[i]=max(nums[i−1]+nums[i],nums[i])
int main()
{
int sum = nums[0];
int max_sum = nums[0];
for(int i=1; i<numsSize; i++)
{
if(sum > 0) // 第 i 天是正数
sum += nums[i];
else // 如果是负数,那抛弃之前的,从当前天开始往后走
sum = nums[i];
max_sum = max(sum, max_sum);
}
return max_sum;
}
三角形的最小路径和
设 f(i, j) 为点 (i, j) 到底部的最小路径和,递推得状态转移方程
f
(
i
,
j
)
=
m
i
n
(
f
(
i
+
1
,
j
)
,
f
(
i
+
1
,
j
+
1
)
)
+
t
r
i
a
n
g
l
e
[
i
]
[
j
]
f(i, j) = min(f(i+1, j), f(i+1, j+1)) + triangle[i][j]
f(i,j)=min(f(i+1,j),f(i+1,j+1))+triangle[i][j]
数字三角形
加了一维用k表示可以对k个数字进行乘运算,并且滚动数组
f
[
j
]
[
u
]
=
m
a
x
(
f
[
j
]
[
u
]
,
f
[
j
−
1
]
[
u
]
)
+
a
[
i
]
[
j
]
;
i
f
(
u
)
f
[
j
]
[
u
]
=
m
a
x
(
f
[
j
]
[
u
]
,
m
a
x
(
f
[
j
]
[
u
−
1
]
,
f
[
j
−
1
]
[
u
−
1
]
)
+
P
∗
a
[
i
]
[
j
]
)
;
f[j][u]=max(f[j][u],f[j-1][u])+a[i][j];\\if(u)f[j][u]=max(f[j][u],max(f[j][u-1],f[j-1][u-1])+P*a[i][j]);
f[j][u]=max(f[j][u],f[j−1][u])+a[i][j];if(u)f[j][u]=max(f[j][u],max(f[j][u−1],f[j−1][u−1])+P∗a[i][j]);
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define all(x) x.begin(),x.end()
#define foo(i,a,b) for(int i=a;i<=b;i++)
#define fro(i,a,b) for(int i=a;i>=b;i--)
#define int long long
const int N=5e5+10;
typedef pair<int,int> PII;
typedef long long LL;
const int P=6;
int n,k;
int a[110][110];
int f[110][11000];
signed main()
{
//cout<<fixed<<setprecision(2);
ios::sync_with_stdio(false); cin.tie(0);
cin>>n>>k;
k=min(k,n);
memset(a,-63,sizeof a);
fro(i,n,1)
{
foo(j,1,i)cin>>a[i][j];
}
memset(f,-63,sizeof f);
f[1][0]=0;//注意此处类似滚动数组版本,用f[1][0]最合适,用f[0][0]时需要一些处理
int ans=-0x3f3f3f3f3f3f3f3f;
//cout<<ans<<endl;
foo(i,1,n)
{
fro(j,i,1)
{
fro(u,k,0)
{
if(u>i)continue;
else
{
f[j][u]=max(f[j][u],f[j-1][u])+a[i][j];
if(u)f[j][u]=max(f[j][u],max(f[j][u-1],f[j-1][u-1])+P*a[i][j]);
}
}
}
}
foo(i,1,n)
{
foo(j,0,k)
{
ans=max(ans,f[i][j]);
}
}
cout<<ans;
}
鸡蛋掉落
反向思考,递推的动态规划想法,可以先列表法实验,状态转移方程
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
1
]
+
1
+
d
p
[
i
−
1
]
[
j
]
总次数
=
失败的检验层数
+
该层
+
成功的检验层数
dp[i][j]=dp[i-1][j-1]+1+dp[i-1][j]\\总次数=失败的检验层数+该层+成功的检验层数
dp[i][j]=dp[i−1][j−1]+1+dp[i−1][j]总次数=失败的检验层数+该层+成功的检验层数
class Solution {
public:
int superEggDrop(int K, int N) {
int remainTestCount = 1;//穷举移动次数(测试的次数)
while (getConfirmFloors(remainTestCount, K) < N){
++remainTestCount;
}
return remainTestCount;
}
//在remainTestCount个测试机会(扔鸡蛋的机会 或者移动的次数),eggsCount个鸡蛋可以确定的楼层数量
int getConfirmFloors(int remainTestCount, int eggsCount){
if (remainTestCount == 1 || eggsCount == 1){(难点三、四)
//如果remainTestCount == 1你只能移动一次,则你只能确定第一楼是否,也就是说鸡蛋只能放在第一楼,如果碎了,则F == 0,如果鸡蛋没碎,则F == 1
//如果eggsCount == 1鸡蛋数为1,它碎了你就没有鸡蛋了,为了保险,你只能从第一楼开始逐渐往上测试,如果第一楼碎了(同上),第一楼没碎继续测第i楼,蛋式你不可能无限制的测试,因为你只能测试remainTestCount次
return remainTestCount;
}
return getConfirmFloors(remainTestCount - 1, eggsCount - 1) + 1 + getConfirmFloors(remainTestCount - 1, eggsCount);
}
};
打家劫舍I
d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] ) d p [ i ] [ 1 ] = d p [ i ] [ 0 ] + a [ i ] d p [ i ] [ 1 ] 为取, d p [ i ] [ 0 ] 为不取 dp[i][0]=max(dp[i-1][0],dp[i-1][1])\\dp[i][1]=dp[i][0]+a[i]\\dp[i][1]为取,dp[i][0]为不取 dp[i][0]=max(dp[i−1][0],dp[i−1][1])dp[i][1]=dp[i][0]+a[i]dp[i][1]为取,dp[i][0]为不取
int rob(int[] nums) {
int n = nums.length;
// 记录 dp[i+1] 和 dp[i+2]
int dp_i_1 = 0, dp_i_2 = 0;
// 记录 dp[i]
int dp_i = 0;
for (int i = n - 1; i >= 0; i--) {
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
打家劫舍II
环装的处理:给第一个做分类,要么偷,倒数第二个必须不偷;要么不偷
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0]; //只有一间房间,返回nums[0]
vector<int>f(n + 1), g(n + 1);
f[1] = nums[0], g[2] = nums[1]; //初始化
for(int i = 2; i <= n - 1; i++) f[i] = max(f[i - 1], f[i - 2] + nums[i - 1]); //区间[1,n-1]最大值
for(int i = 3; i <= n; i++) g[i] = max(g[i - 1], g[i - 2] + nums[i - 1]); //区间[2,n]最大值
return max(f[n - 1], g[n]);
}
};
买卖股票的最佳时机I
只能买卖一次,前后取差值,dp找最大的连续最大数列
或者
d
p
[
0
]
[
j
]
=
m
a
x
(
d
p
[
0
]
[
j
−
1
]
,
d
p
[
1
]
[
j
−
1
]
+
p
r
i
c
e
s
[
j
]
)
d
p
[
1
]
[
j
]
=
m
a
x
(
d
p
[
1
]
[
j
−
1
]
,
−
p
r
i
c
e
s
[
j
]
)
其中
0
指不持股,
1
指持股
dp[0][j] = max(dp[0][j-1], dp[1][j-1] + prices[j])\\dp[1][j] = max(dp[1][j-1], -prices[j])\\其中0指不持股,1指持股
dp[0][j]=max(dp[0][j−1],dp[1][j−1]+prices[j])dp[1][j]=max(dp[1][j−1],−prices[j])其中0指不持股,1指持股
def maxProfit_opt(self, prices):
size = len(prices)
if size == 0 or size == 1:
return 0
dp1 = 0
dp2 = -prices[0]
for j in range(1, size)://空间优化
tmp1 = max(dp1, dp2+prices[j])
tmp2 = max(dp2, -prices[j])
dp1, dp2 = tmp1, tmp2
return dp1
买卖股票的最佳时机II
不限制购买,前后取差值,累加大于0的数字
或者:
d
p
[
0
]
[
j
]
=
m
a
x
(
d
p
[
0
]
[
j
−
1
]
,
d
p
[
1
]
[
j
−
1
]
+
p
r
i
c
e
[
j
]
)
;
d
p
[
1
]
[
j
]
=
m
a
x
(
d
p
[
1
]
[
j
−
1
]
,
d
p
[
0
]
[
j
−
1
]
−
p
r
i
c
e
[
j
]
)
其中
0
指不持股,
1
指持股
dp[0][j]=max(dp[0][j-1],dp[1][j-1]+price[j]);\\dp[1][j]=max(dp[1][j-1],dp[0][j-1]-price[j])\\其中0指不持股,1指持股
dp[0][j]=max(dp[0][j−1],dp[1][j−1]+price[j]);dp[1][j]=max(dp[1][j−1],dp[0][j−1]−price[j])其中0指不持股,1指持股
def maxProfit_opt(self, prices):
size = len(prices)
if size == 0 or size == 1:
return 0
# 初始化动态数组
dp1 = 0
dp2 = -prices[0]
for j in range(1, size)://空间优化
tmp1 = max(dp1, dp2 + prices[j])
tmp2 = max(dp2, dp1 - prices[j])
dp1, dp2 = tmp1, tmp2
return dp1
买卖股票的最佳时机III
交易两次,求最大利润,加一维
d
p
[
i
]
[
j
]
=
m
a
x
{
d
p
[
i
]
[
j
−
1
]
,
m
a
x
{
p
r
i
c
e
s
[
i
]
−
p
r
i
c
e
s
[
n
]
+
d
p
[
i
−
1
]
[
n
]
}
}
,
n
=
0
,
1
,
…
,
j
−
1
dp[i][j]=max\left \{dp[i][j-1], max\left \{ prices[i]-prices[n]+dp[i-1][n] \right \} \right \} , n=0,1,…,j-1
dp[i][j]=max{dp[i][j−1],max{prices[i]−prices[n]+dp[i−1][n]}},n=0,1,…,j−1
买卖股票的最佳时机Ⅳ
如上题
d
p
[
i
]
[
j
]
=
m
a
x
{
d
p
[
i
]
[
j
−
1
]
,
m
a
x
{
p
r
i
c
e
s
[
i
]
−
p
r
i
c
e
s
[
n
]
+
d
p
[
i
−
1
]
[
n
]
}
}
,
n
=
0
,
1
,
…
,
j
−
1
dp[i][j]=max\left \{dp[i][j-1], max\left \{ prices[i]-prices[n]+dp[i-1][n] \right \} \right \} , n=0,1,…,j-1
dp[i][j]=max{dp[i][j−1],max{prices[i]−prices[n]+dp[i−1][n]}},n=0,1,…,j−1
def maxProfit(self, k, prices):
size = len(prices)
if size == 0:
return 0
dp = [[0 for _ in range(size)] for _ in range(k+1)]
print(dp)
for i in range(1, k+1):
# 每一次交易的最大利润
max_profit = -prices[0]
for j in range(1, size):
dp[i][j] = max(dp[i][j-1], max_profit + prices[j])
max_profit = max(max_profit, dp[i-1][j] - prices[j])
print(dp)
return dp[-1][-1]
买卖股票的最佳时机含冷冻期
状态转移方程
d
p
[
0
]
[
j
]
=
m
a
x
(
d
p
[
0
]
[
j
−
1
]
,
d
p
[
2
]
[
j
−
1
]
)
d
p
[
1
]
[
j
]
=
m
a
x
(
d
p
[
0
]
[
j
−
1
]
−
p
r
i
c
e
[
j
]
,
d
p
[
1
]
[
j
−
1
]
)
d
p
[
2
]
[
j
]
=
d
p
[
1
]
[
j
−
1
]
+
p
r
i
c
e
[
j
]
i
=
0
表示未购状态,
i
=
1
表示持有状态,
i
=
2
表示冷却状态
dp[0][j]=max(dp[0][j-1],dp[2][j-1])\\dp[1][j]=max(dp[0][j-1]-price[j],dp[1][j-1])\\dp[2][j]=dp[1][j-1]+price[j]\\i=0表示未购状态,i=1表示持有状态,i=2表示冷却状态
dp[0][j]=max(dp[0][j−1],dp[2][j−1])dp[1][j]=max(dp[0][j−1]−price[j],dp[1][j−1])dp[2][j]=dp[1][j−1]+price[j]i=0表示未购状态,i=1表示持有状态,i=2表示冷却状态
买卖股票的最佳时机含手续费
d p [ 0 ] [ j ] = m a x ( d p [ 0 ] [ j − 1 ] , d p [ 1 ] [ j − 1 ] + p r i c e [ j ] − f e e ) ; d p [ 1 ] [ j ] = m a x ( d p [ 1 ] [ j − 1 ] , d p [ 0 ] [ j − 1 ] − p r i c e [ j ] ) 其中 0 指不持股, 1 指持股 dp[0][j]=max(dp[0][j-1],dp[1][j-1]+price[j]-fee);\\dp[1][j]=max(dp[1][j-1],dp[0][j-1]-price[j])\\其中0指不持股,1指持股 dp[0][j]=max(dp[0][j−1],dp[1][j−1]+price[j]−fee);dp[1][j]=max(dp[1][j−1],dp[0][j−1]−price[j])其中0指不持股,1指持股
区间dp
最长回文子串
正着反着都一样的一个区域就是回文区域,即找双串的最长相同串,时间复杂度O(n2)
最长回文子序列(LPS)
正着和反着的最长公共子序列
背包dp
是比较简单的一种dp类型,写之前先列一组数据算一算,列出方程和初始状态
01背包
背包有固定容量,判断取不取物体(物体只有一个)以达到最大的价值
v
a
l
u
e
[
i
]
[
j
]
=
m
a
x
(
v
a
l
u
e
[
i
]
[
j
]
,
v
a
l
u
e
[
i
−
1
]
[
j
−
u
s
e
d
[
j
]
]
+
v
a
l
[
j
]
)
value[i][j]=max(value[i][j],value[i-1][j-used[j]]+val[j])
value[i][j]=max(value[i][j],value[i−1][j−used[j]]+val[j])
洛谷 P1048 [NOIP2005 普及组] 采药
#include<bits/stdc++.h>
using namespace std;
#define foo(i,a,b) for(int i=a;i<b;i++)
const int N=130;
int n,m,f[1005];
struct herb
{
int time,value;
}h[N];
signed main()
{
cin>>n>>m;
foo(i,1,m+1)
{
cin>>h[i].time>>h[i].value;
}
foo(i,1,m+1)
{
for(int j=n;j>=1;j--)
{
if(j>=h[i].time) f[j]=max(f[j-h[i].time]+h[i].value,f[j]);//二维转一维要看数据怎么替换的
else f[j]=f[j];
}
}
cout<<f[n];
return 0;
}
完全背包
背包有固定容量,判断取不取物体(物体有无限个)以达到最大的价值
f
i
r
s
t
:
v
a
l
u
e
[
i
]
[
j
]
=
m
a
x
(
v
a
l
u
e
[
i
]
[
j
]
,
v
a
l
u
e
[
i
−
1
]
[
j
−
u
s
e
d
[
j
]
]
+
v
a
l
[
j
]
)
s
e
c
o
n
d
:
v
a
l
u
e
[
i
]
[
j
]
=
m
a
x
(
v
a
l
u
e
[
i
]
[
j
]
,
v
a
l
u
e
[
i
]
[
j
−
u
s
e
d
[
j
]
]
+
v
a
l
[
j
]
)
first:value[i][j]=max(value[i][j],value[i-1][j-used[j]]+val[j])\\ second:value[i][j]=max(value[i][j],value[i][j-used[j]]+val[j])
first:value[i][j]=max(value[i][j],value[i−1][j−used[j]]+val[j])second:value[i][j]=max(value[i][j],value[i][j−used[j]]+val[j])
洛谷 P1616 疯狂的采药
#include<bits/stdc++.h>
using namespace std;
#define foo(i,a,b) for(int i=a;i<b;i++)
const signed N=1e7+10,M=1e4+10;
int t,m,f[N];
struct herb
{
int time,value;
}h[M];
signed main()
{
cin>>t>>m;
foo(i,1,m+1)
{
cin>>h[i].time>>h[i].value;
}
foo(i,1,m+1)
{
foo(j,h[i].time,t+1)
{
f[j]=max(f[j],f[j-h[i].time]+h[i].value);//顺着来正好实现功能
}
}
cout<<f[t];
return 0;
}
多重背包
背包有固定容量,判断取不取物体(物体有有限个)以达到最大的价值,有两种思路,一种是二进制拆分、另一种是单调队列优化
二进制拆分就是把多重背包用完全二进制的思想拆成01背包
洛谷 P1776 宝物筛选
#include<bits/stdc++.h>
using namespace std;
#define foo(i,a,b) for(int i=a;i<b;i++)
#define fio(i,a,b) for(int i=a;i>b;i--)
const int N=1e5+10;
int n,W,f[N],v[N],w[N];
signed main()
{
cin>>n>>W;
int cnt,i=1,isq=1,mark,temp,add;
while(i<=n)
{
cin>>v[isq]>>w[isq]>>cnt;
temp=cnt;
mark=1,add=0;
while(temp>=mark)//二进制拆分
{
temp-=mark;
v[isq+add]=v[isq]*mark;
w[isq+add]=w[isq]*mark;
mark*=2;
add++;
}
if(temp!=0)
{
v[isq+add]=v[isq]*temp;
w[isq+add]=w[isq]*temp;
add++;
}
isq=isq+add;
i++;
}
n=isq-1;
foo(ii,1,n+1)//01背包
{
fio(j,W,w[ii]-1)
{
f[j]=max(f[j],f[j-w[ii]]+v[ii]);
}
}
cout<<f[W];
return 0;
}
单调队列优化(容易出错,不如直接二进制拆分)
洛谷 P1776 宝物筛选
#include<bits/stdc++.h>
#include<bits/stdc++.h>
using namespace std;
const int N=5e4;
int n,W,v,w,m;//价值、重量、个数
int dp[N],q1[N],q2[N],head,tail,ans;
int main()
{
cin>>n>>W;
for(int i=0;i<n;i++)
{
cin>>v>>w>>m;
if(w==0)
{
ans+=v*m;
continue;
}
int group=min(W/w,m);
for(int d=0;d<w;d++)
{
head=0,tail=0;
for(int j=0;j*w+d<=W;j++)
{
while(head<tail&&dp[d+j*w]-j*v>=q2[tail-1])
{
tail--;
}
q1[tail]=j;
q2[tail]=dp[d+j*w]-j*v;
tail++;
while(head<tail&&q1[head]<j-group)
{
head++;
}
dp[d+j*w]=max(dp[d+j*w],q2[head]+j*v);
}
}
}
cout<<ans+dp[W];
return 0;
}
二维费用背包
一样的思路,只是多加一个维度选择
P1855 榨取kkksc03
#include<bits/stdc++.h>
using namespace std;
int n,m,t,dp[210][210];
int main()
{
cin>>n>>m>>t;
int tempm,tempt;
for(int i=0;i<n;i++)
{
cin>>tempm>>tempt;
for(int mm=m;mm>=tempm;mm--)
{
for(int tt=t;tt>=tempt;tt--)
{
dp[mm][tt]=max(dp[mm][tt],dp[mm-tempm][tt-tempt]+1);
}
}
}
cout<<dp[m][t];
return 0;
}
分组背包
把组作为最外循环,组中的元素作为最内侧循环,保证了只有一个的插入情况,注意理清楚每一组之间的数量
P1757 通天之分组背包
#include<bits/stdc++.h>
using namespace std;
int n,m,dp[1500];
struct things
{
int weight;
int value;
int group;
}t[1500];
bool cmp(things a,things b)
{
return a.group<b.group;
}
int main()
{
cin>>m>>n;
if(m==0)//特判m=0的情况
{
cout<<"0";
exit(0);
}
for(int i=0;i<n;i++)
{
cin>>t[i].weight>>t[i].value>>t[i].group;
}
bool flag;
sort(t,t+n,cmp);
t[n].weight=20000;t[n].value=0;t[n].group=20000;
for(int p=0;p<n;)
{
//cout<<"p=外层"<<p<<endl;
int markp=p;
for(int j=m;j>0;j--)
{
p=markp;
flag=true;
for(;flag||t[p].group==t[p+1].group;p++)
{
if(flag==false) break;
//cout<<"flag=" <<flag<<endl;
//cout<<"p="<<p<<endl;
if(j>=t[p].weight) dp[j]=max(dp[j],dp[j-t[p].weight]+t[p].value);
if(t[p].group!=t[p+1].group) flag=false;
}
}
}
cout<<dp[m];
return 0;
}
有依赖的背包
一般思路:如果背包的附件比较少,可以枚举出来,可以转化为01背包类似的想法做,但是依赖的个数渐进是指数级的
P1064 [NOIP2006 提高组] 金明的预算方案
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e4;
int n,m;
int v,p,q;
int main_item_w[maxn];
int main_item_c[maxn];
int annex_item_w[maxn][3];//这种分组的对应方式很清爽
int annex_item_c[maxn][3];
int f[maxn];
int main(){
cin >> n >> m;
for (int i=1;i<=m;i++){
cin >> v >> p >> q;
if(!q)
{
main_item_w[i] = v;
main_item_c[i] = v * p;
}
else
{
annex_item_w[q][0]++;
annex_item_w[q][annex_item_w[q][0]] = v;
annex_item_c[q][annex_item_w[q][0]] = v * p;
}
}
for (int i=1;i<=m;i++)
{
for (int j=n;main_item_w[i]!=0 && j>=main_item_w[i];j--){
f[j] = max(f[j],f[j-main_item_w[i]]+main_item_c[i]);
if (j >= main_item_w[i] + annex_item_w[i][1])
f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] ] + main_item_c[i] + annex_item_c[i][1]);
if (j >= main_item_w[i] + annex_item_w[i][2])
f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][2]);
if (j >= main_item_w[i] + annex_item_w[i][1] + annex_item_w[i][2])
f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][1] + annex_item_c[i][2]);
}
}
cout << f[n];
return 0;
}
组合总和 Ⅳ
状态转移方程
d
p
[
i
]
[
t
]
=
d
p
[
i
−
1
]
[
t
]
+
d
p
[
i
−
1
]
[
t
−
s
[
i
]
]
dp[i][t]=dp[i-1][t]+dp[i-1][t-s[i]]
dp[i][t]=dp[i−1][t]+dp[i−1][t−s[i]]
数位dp
本质上是一种用dp数组来记录的记忆化搜索
#include<bits/stdc++.h>
using namespace std;
int mem[15][15];
int dfs(string x,int pos,int past,bool zero,bool f)
{
if(pos>=x.length())
{
return 1;
}
//cout<<"x= "<<x<<" "<<pos<<" "<<past<<" "<<zero<<" "<<f<<endl;
if(zero!=0&&f!=0&&mem[pos][past]!=0)
{
//cout<<"I rem"<<"pos="<<pos<<" "<<past<<" "<<mem[pos][past]<<endl;
return mem[pos][past];
}
int maxn=(f==false?(x[pos]-'0'):9);
//cout<<"maxn= "<<maxn<<endl;
int res=0;
for(int i=0;i<=maxn;i++)
{
if(f==false)
{
if(zero==0)
{
res+=dfs(x,pos+1,i,i==0?0:1,f||i!=maxn);
}
else
{
if(abs(i-past)>=2)
{
res+=dfs(x,pos+1,i,zero,f||i!=maxn);
}
}
}
else
{
if(zero==0)
{
res+=dfs(x,pos+1,i,i==0?0:1,1);
}
else if(abs(i-past)>=2)
{
res+=dfs(x,pos+1,i,1,1);
}
}
}
if(zero!=0&&f!=0)mem[pos][past]=res;
return res;
}
int main()
{
// for(int i=10000;i<=10222;i++)
// {
// cout<<i<<" "<<dfs(to_string(i),0,0,0,0)<<endl;
// for(int j=0;j<15;j++)
// {
// for(int k=0;k<15;k++)
// {
// mem[j][k]=0;
// }
// }
// }
int a,b;
cin>>a>>b;
int t1=dfs(to_string(b),0,0,0,0);
for(int j=0;j<15;j++)
{
for(int k=0;k<15;k++)
{
mem[j][k]=0;
}
}
int t2=dfs(to_string(a-1),0,0,0,0);
cout<<t1-t2;
return 0;
}
概率dp
拿前面的概率做后面的状态转移
#include<bits/stdc++.h>
using namespace std;
#define int double
int dp[1010][1010];
signed main()
{
signed a,b;
cin>>a>>b;
for(signed i=1;i<=a;i++)
{
dp[i][0]=1;
}
for(signed i=0;i<=b;i++)
{
dp[0][i]=0;
}
for(signed i=1;i<=a;i++)
{
for(signed j=1;j<=b;j++)
{
dp[i][j]=i*1.0/(i+j);
if(i>=1&&j>=2)
{
dp[i][j]+=j*1.0*(j-1)*i/(i+j)/(i+j-1)/(i+j-2)*dp[i-1][j-2];
}
if(j>=3)
{
dp[i][j]+=j*1.0*(j-1)*(j-2)/(i+j)/(i+j-1)/(i+j-2)*dp[i][j-3];
}
}
}
printf("%.9lf",dp[a][b]);
return 0;
}
求期望的时候要专注于状态转移,状态转移是动态规划的精髓,可能用到的数学知识很多,有复杂的数学计算,期望dp每一个空间存储的是达到的期望值
状态压缩dp
把一长串事情压缩成一个数字单独表示
#include<bits/stdc++.h>
using namespace std;
string a[20],b[20];
bool dp[70000][20],connect[20][20];
int n;
void dfs(int x,int y)
{
dp[x][y]=1;
//cout<<"B"<<x<<" "<<y<<endl;
for(int i=0;i<n;i++)
{
//cout<<"pre"<<"i="<<i<<" "<<((1<<i)&x)<<" "<<connect[y][i]<<endl;
if((!((1<<i)&x))&&connect[y][i])
{
//cout<<"C"<<endl;
if(!dp[1<<i|x][i])
dfs((1<<i)|x,i);
}
}
}
void solve()
{
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i]>>b[i];
}
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
connect[i][j]=(a[i]==a[j]||b[i]==b[j]);
}
}
// cout<<(1<<n)<<endl;
for(int i=0;i<(1<<n);i++)
{
for(int j=0;j<n;j++)
{
dp[i][j]=false;
}
}
for(int i=0;i<n;i++)
{
dp[1<<i][i]=1;
dfs(1<<i,i);
}
int ans=0;
for(int i=0;i<(1<<n);i++)
{
for(int j=0;j<n;j++)
{
//cout<<"dp "<<i<<" "<<j<<" "<<__builtin_popcount(i)<<endl;
if(dp[i][j]) ans=max(ans,__builtin_popcount(i));
}
}
cout<<n-ans<<endl;
}
signed main(void)
{
int T;
cin>>T;
while(T--)
{
solve();
}
return 0;
}