动态规划

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值