date: 2019-07-01 11:18:40
动态规划
核心在于状态的表示和状态的转移
1. 背包问题
2.线性dp
例1:数字三角形
输入
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出
30
思路
dp的时间复杂度计算一般是状态数*转移的计算次数
本题状态数n2,转移计算量O(1),所以总时间复杂度为O(n2)
注意:三角形中的整数可以为负数,所以初始化的时候要负得足够多
从下往上 把(1,1)当成唯一终点
#include <iostream>
using namespace std;
const int maxn=505;
const int INF=1e8;
int dp[maxn][maxn],a[maxn][maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>a[i][j];
for(int i=n;i>=1;i--)
for(int j=i;j>=1;j--)
dp[i][j]=max(dp[i+1][j]+a[i][j],dp[i+1][j+1]+a[i][j]);
cout<<dp[1][1]<<endl;
return 0;
}
从上往下 需要遍历最底层所有终点
#include <iostream>
using namespace std;
const int maxn=505;
const int INF=1e8;
int dp[maxn][maxn],a[maxn][maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>a[i][j];
for(int i=0;i<=n;i++)//注意初始化要从0开始,因为dp[i-1]
for(int j=0;j<=i+1;j++)
dp[i][j]=-INF;
dp[1][1]=a[1][1];
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
dp[i][j]=max(dp[i-1][j-1]+a[i][j],dp[i-1][j]+a[i][j]);
int ans=-INF;
for(int i=1;i<=n;i++)
ans=max(ans,dp[n][i]);
cout<<ans<<endl;
return 0;
}
WA代码:没有注意到数据范围可为负,如果输入的为负数,那么初始化为全0就会导致问题
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dp[i][j]=max(dp[i-1][j-1]+a[i][j],dp[i-1][j]+a[i][j]);
}
例2:最长上升子序列
n个状态,转移时要枚举n,所以时间复杂度为O(n2)
#include <iostream>
using namespace std;
const int maxn=1e3+5;
int dp[maxn];
int a[maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
dp[i]=1;//初始化,最开始时只有自己
for(int j=1;j<=i;j++)
if(a[j]<a[i])
dp[i]=max(dp[i],dp[j]+1);
}
int ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,dp[i]);
cout<<ans<<endl;
return 0;
}
记录最长上升子序列
#include <iostream>
using namespace std;
const int maxn=1e3+5;
int dp[maxn];
int a[maxn],pre[maxn];//ind用来记录转移的路径
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
dp[i]=1;//初始化,最开始时只有自己
pre[i]=0;
for(int j=1;j<=i;j++)
if(a[j]<a[i])
{
if(dp[i]<dp[j]+1)
{
dp[i]=dp[j]+1;
pre[i]=j;//记录每一次从哪里转移而来
}
}
}
int k=1;
for(int i=1;i<=n;i++)
if(dp[i]>dp[k])
k=i;
cout<<dp[k]<<endl;//找到最大值
int len=dp[k];
int s[maxn];
for(int i=0;i<len;i++)
{
s[i]=a[k];
//cout<<a[k]<<" ";//倒序输出
k=pre[k];//它由谁转移而来
}
for(int i=len-1;i>=0;i--)
cout<<s[i]<<" ";
cout<<endl;
return 0;
}
二分加速
如果数据量变成了1≤n≤100000
,O(n2) 就变成1010接受不了了
O(nlogn)
#include <iostream>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int mod=1e9+7;
int a[maxn],y[maxn];//y里存储末尾最小的数
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
int len=0;//存储最长上升子序列长度
y[0]=-2e9;
for(int i=0;i<n;i++)//遍历每一个数
{
int l=0,r=len;
while(l<r)
{
int mid=l+r+1>>1;
if(y[mid]<a[i])
l=mid;
else
r=mid-1;
}
//r找的是第1个<a[i]的y的下标,即a[i]可以接在该数后面
len=max(len,r+1);//更新最大长度
y[r+1]=a[i];//原来的y[r+1]≥a[i]
}
cout<<len<<endl;
return 0;
}
也可以模拟栈
#include <iostream>
#include <cmath>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int mod=1e9+7;
int a[maxn],y[maxn];//y里存储末尾最小的数
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
vector<int>st;
st.push_back(a[0]);//模拟栈
for(int i=1;i<n;i++)
{
if(a[i]>st.back())
st.push_back(a[i]);
else//这个数≤栈顶,就替换掉栈里第一个大于或者等于a[i]的那个数
{
vector<int>::iterator it=lower_bound(st.begin(),st.end(),a[i]);
(*it)=a[i];
}
}
cout<<st.size()<<endl;
return 0;
}
例3:最长公共子序列
小集合的最大值≤大集合的最大值,所以01的最大值≤dp[i-1][j],10同理,所以dp[i-1][j]和dp[i][j-1]就可以覆盖掉01和10两部分最大值
虽然dp[i-1,j]
的最大值不一定是01这种情况,但对答案是没影响的,因为dp[i-1,j]
中的方案一定是f[i,j]
中的方案。
#include <iostream>
#include <string>
const int maxn=1e3+5;
using namespace std;
int dp[maxn][maxn];
int main()
{
int n,m;
cin>>n>>m;
char s[maxn],t[maxn];
cin>>s+1;//如果s声明成string类就不能直接s+1
cin>>t+1;
for(int i=1;i<=n;i++)//如果从0开始,会导致数组越界到-1
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(s[i]==t[j])
dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);//相当于三部分取max
}
cout<<dp[n][m]<<endl;
return 0;
}
例4:最短编辑距离
输入
10
AGTCTGACGC
11
AGTAAGTAGGC
输出
4
题解
#include <iostream>
using namespace std;
const int maxn=1e3+5;
char a[maxn],b[maxn];
int dp[maxn][maxn];
int main()
{
int n,m;
scanf("%d",&n);
scanf("%s",a+1);
scanf("%d",&m);
scanf("%s",b+1);
for(int i=0;i<=n;i++)//一定要注意边界情况
dp[i][0]=i;
for(int j=0;j<=m;j++)
dp[0][j]=j;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=min(dp[i-1][j]+1,dp[i][j-1]+1);
if(a[i]==b[j])
dp[i][j]=min(dp[i][j],dp[i-1][j-1]);
else
dp[i][j]=min(dp[i][j],dp[i-1][j-1]+1);
}
printf("%d\n",dp[n][m]);
return 0;
}
例4变形:编辑距离
输入
3 2
abc
acd
bcd
ab 1
acbd 2
输出
1
3
题解
基本同上。
#include <iostream>
#include <cstring>
using namespace std;
int maxn=15;
char str[1005][15];
int compute(char a[],char b[])
{
int dp[maxn][maxn];
int n,m;
n=strlen(a+1),m=strlen(b+1);
for(int i=0;i<=n;i++)//一定要注意边界情况
dp[i][0]=i;
for(int j=0;j<=m;j++)
dp[0][j]=j;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=min(dp[i-1][j]+1,dp[i][j-1]+1);
if(a[i]==b[j])
dp[i][j]=min(dp[i][j],dp[i-1][j-1]);
else
dp[i][j]=min(dp[i][j],dp[i-1][j-1]+1);
}
return dp[n][m];
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
scanf("%s",str[i]+1);
while(m--)
{
char s[maxn];
int up;
scanf("%s%d",s+1,&up);
int ans=0;
for(int i=0;i<n;i++)
if(compute(s,str[i])<=up)
ans++;
printf("%d\n",ans);
}
return 0;
}
3.区间dp
区间dp一般会从小到大枚举区间大小
,再循环枚举区间的左端点,再枚举决策
例题 合并相邻
石子堆
输入
4
1 3 5 2
输出
22
思路
状态数量n2,状态计算只需要枚举k,计算量为n,所以总时间复杂度为O(n3)
#include <iostream>
#include <cmath>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=305;
const int mod=1e9+7;
int s[maxn],dp[maxn][maxn];//y里存储末尾最小的数
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>s[i];
for(int i=1;i<=n;i++)
s[i]+=s[i-1];//求前缀和
//区间为1,合并不需要代价,所以可以从2开始
for(int num=2;num<=n;num++)//枚举区间长度,即所需合并石子总堆数
for(int i=1;i+num-1<=n;i++)//枚举起点
{
int l=i,r=i+num-1;
dp[l][r]=1e8;//一定要注意这里的初始化
for(int k=l;k<r;k++)//枚举分界点
dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]-s[l-1]);
}
cout<<dp[1][n]<<endl;
return 0;
}
因为要保证要求的区间内的所有状态都已经被求好
,所以枚举区间大小
4.计数类dp
整数划分问题
输入
5
输出
7
思路1:背包写法
容量为n的背包,物品体积分别为1,2,…,n,求恰好装满背包的方案数。每个物品可以用无限次,所以是完全背包问题
。
#include <iostream>
using namespace std;
const int maxn=1e3+5;
const int mod=1e9+7;
int dp[maxn];
int main()
{
int n;
cin>>n;
dp[0]=1;//一定要注意这个初始化情况
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)//
dp[j]=(dp[j]+dp[j-i])%mod;
cout<<dp[n]<<endl;
return 0;
}
思路2:
#include <iostream>
using namespace std;
const int maxn=1e3+5;
const int mod=1e9+7;
int dp[maxn][maxn];
int main()
{
int n;
cin>>n;
dp[0][0]=1;//一定要注意这个初始化情况
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
dp[i][j]=(dp[i-1][j-1]+dp[i-j][j])%mod;
int ans=0;
for(int i=1;i<=n;i++)
ans=(ans+dp[n][i])%mod;//注意取模
cout<<ans<<endl;
return 0;
}