动态规划入门专题

 DP:简单来说就是寻找最终状态与前面状态的关系式,将最初的状态叠加到最后状态,从而得出答案

下面是一些经典DP题,建议先搞懂背包问题中的各种优化,可见我另一篇博客:背包问题

题目目录

                斐波那契序列

                 三角形数

                最小路径和

                将字符串翻转到单调递增

                最长匹配括号


斐波那契序列

题目:后一个数等于前两个数的和,1,1,2,3,5,8,13,21······

说明:可以说是最经典的DP题了,f(n)=f(n-1)+f(n-2);

long long f(int n)
{
    if(n==1||n==2) return 1;
    return f(n-1)+f(n-2);
}

cout<<f(n)<<endl;

进阶:这种递归写法时间复杂度很大,这是因为每次都要重新计算f(n),可以优化成有记忆性的递归,即每次都记录下已经算过的f(n)

long long f[1005];
long long dp(int n)
{
    if(f[n]!=0) return f[n];
    if(n==1||n==2) return 1;
    f[n]=dp(n-1)+dp(n-2);
    return f[n];
}

cout<<dp(n)<<endl;

三角形数

题目

输入n行数,第n行有n个数,从上往下走最长的路径,注意给的数有可能是负数

输入
4
1
2 4
15 5 8
7 6 9 15
输出

28

题目链接数字三角形

 思路: 从下往上递最优的状态

代码

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<iostream>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define M 1005
int n;
int a[M][M];
int dp[M];     //一维优化
int main()
{
    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=1;j<=i;j++)
            dp[j]=max(dp[j],dp[j+1])+a[i][j];
    cout<<dp[1]<<endl;
    return 0;
}


进阶:1.求最小路径

 思路:将max改为min即可

for(int i=n;i>=1;i--)
        for(int j=1;j<=i;j++)
            dp[j]=min(dp[j],dp[j+1])+a[i][j];
    cout<<dp[1]<<endl;

          2.从上往下实现(将状态递到最后一行,MAX筛选最优的)

#include<iostream>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define M 1005
int n,MAX=0;
int a[M][M];
int dp[M];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
    memset(dp,-inf,sizeof(dp));
    dp[1]=0;
    for(int i=1;i<=n;i++)
        for(int j=i;j>=1;j--)
            dp[j]=max(dp[j],dp[j-1])+a[i][j];
    for(int j=1;j<=n;j++)
        MAX=max(dp[j],MAX);
    cout<<MAX<<endl;
    return 0;
}


最小路径和

题目:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。

输入

3 4
1 3 1 2
1 5 10 5
2 13 3 20
输出

27
解释: 因为路径 1→3→1→2→5→20 的总和最小。

题目链接最小路径和

思路:(n,m)等于左边的(n-1,m)和上面的(n,m-1)中最小的+a[n][m]

AC代码:

#include<iostream>
#include<algorithm>
using namespace std;
#define M 1005
int n,m;
int a[M][M];
int dp[M][M];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
             dp[i][j]=min(dp[i-1][j],dp[i][j-1])+a[i][j];
    cout<<dp[n][m]<<endl;
    return 0;
}


进阶:一维优化

for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++) //要用左边dp和上边dp的来判断该dp,而左边的dp是同行,故正序
             dp[j]=min(dp[j],dp[j-1])+a[i][j];
    cout<<dp[m]<<endl;

将字符串翻转到单调递增

题目

如果一个由 ‘0’ 和 ‘1’ 组成的字符串,是以一些 ‘0’(可能没有 ‘0’)后面跟着一些 ‘1’(也可能没有 ‘1’)的形式组成的,那么该字符串是单调递增的。

我们给出一个由字符 ‘0’ 和 ‘1’ 组成的字符串 S,我们可以将任何 ‘0’ 翻转为 ‘1’ 或者将 ‘1’ 翻转为 ‘0’。

返回使 S 单调递增的最小翻转次数。

样例 1:
输入:“01110”
输出:1
解释:翻转最后一位得到 01111.
样例 2:
输入:“010110”
输出:2
解释:翻转得到 011111,或者是 000111。
样例 3:
输入:“00011100”
输出:2
解释:翻转得到 00011111。

题目链接将字符串翻转到单调递增

思路:考虑加新的数与不加的关系,加的是1一定不变,加的是0就考虑前面最优是全部翻转成0,或最后有1两种情况

LeetCode代码:

class Solution {
public:
    int minFlipsMonoIncr(string s) {
        int n=s.size();
        int dp[20005];
        int sum[20005]={0};
        for(int i=1;i<=n;i++)
            sum[i]=sum[i-1]+s[i-1]-'0';    ///前缀和记录前面1的个数
        for(int i=1;i<=n;i++){
            if(s[i-1]=='1') dp[i]=dp[i-1];
            else dp[i]=min(sum[i],dp[i-1]+1);  ///要么全部翻转成0(sum[1]),要么最后有1(dp[i-1]+1)
        }
        return dp[n];
    }
};

AC代码:

#include <iostream>
using namespace std;
#define M 20005
int dp[M];
string s;
int main()
{
    cin>>s;
    int n=s.size();
    int sum[20005]={0};
    for(int i=1;i<=n;i++)
        sum[i]=sum[i-1]+s[i-1]-'0';    ///前缀和记录前面1的个数
    for(int i=1;i<=n;i++){
        if(s[i-1]=='1') dp[i]=dp[i-1];
        else dp[i]=min(sum[i],dp[i-1]+1);  ///要么全部翻转成0(sum[1]),要么最后有1(dp[i-1]+1)
    }
    cout<<dp[n]<<endl;
    return 0;
}

进阶:使用背包问题的优化思想,将上面的代码从一维优化到单变量

核心代码:

for(int i=1;i<=n;i++)
        if(s[i-1]=='0') dp=min(sum[i],dp+1);
    cout<<dp<<endl;

最长匹配括号

题目

输入一个只包含" ( "  " ) "  " [ "  " ] " 的字符串,输出可以成功匹配的括号数量,注意中间有其他括号也可以进行匹配(如果必须中间没有其他括号才能匹配就是用栈来做的)

输入

((()))
()()()
([]])
)[)(
([][][)
end

输出

6
6
4
0
6

 题目链接最长匹配括号

思路:枚举左右区间下标,区间间距逐渐变大,dp[i][j]是区间 i到j 匹配的括号数, 所以dp[i][j]至少=中间所有拆成两部分的小区间之和的最大值

核心DP:如果匹配成功,dp[左边界][右边界]=dp[左边界+1][右边界-1]+2;

代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<string>
using namespace std;
#define inf 0x3f3f3f3f;
#define M 1005
string s;
int dp[M][M];
int main()
{
	while(cin>>s&&s!="end")    //s下标是从0到len-1
	{
		memset(dp,0,sizeof(dp));
		int len=s.size();
		for(int i=1;i<len;i++){
			for(int j=0;j+i<len;j++){
				int k=j+i;    ///j是左下标,k是右下标,i是左右下标的间距
				if((s[j]=='('&&s[k]==')')||(s[j]=='['&&s[k]==']'))
					dp[j][k]=dp[j+1][k-1]+2;    //核心DP
				for(int o=j; o<k; o++)
					dp[j][k]=max(dp[j][k],dp[j][o]+dp[o+1][k]);
			}
		}
		cout<<dp[0][len-1]<<endl;
	}
}

猜你喜欢

转载自blog.csdn.net/m0_58177653/article/details/123963392
今日推荐