动态规划算法(一)

17 篇文章 0 订阅

动态规划例题

在这里插入图片描述
解题过程
例题
在这里插入图片描述
1.确定状态

解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i] [j]代表什么

确定状态需要两个意识:

–最后一步

在这里插入图片描述
–子问题
在这里插入图片描述
在这里插入图片描述
2.转移方程
3.初始条件和边界条件

初始条件,转移方程算不出来,就需要自己定义
在这里插入图片描述

4.计算顺序
在这里插入图片描述

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=110;
const int INF=1<<30;
int m,n,f[maxn],A[maxn];
int main()
{
    // 输入钱数,和右的面值数
    cin>>n>>m;
    // 输入钱数
    for(int i=0;i<m;i++)
        cin>>A[i];
    // 初始化数组    
    for(int i=1;i<=n;i++)
    {
        f[i]=INF;
        for(int j=0;j<m;j++)
        {
            if(i>=A[j]&&f[i-A[j]]!=INF)
                f[i]=min(f[i-A[j]]+1,f[i]);
        }
    }
    cout<<f[n];


    return 0;
}
/*
11 3
1 2 5


3
*/

最大子段和问题

P1115 最大子段和

题目描述

给出一个长度为 n n n 的序列 a a a,选出其中连续且非空的一段使得这段和最大。

输入格式

第一行是一个整数,表示序列的长度 n n n

第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 a i a_i ai

输出格式

输出一行一个整数表示答案。

输入输出样例

输入

7
2 -4 3 -1 2 -4 3

**输出 **

4

说明/提示

样例 1 解释

选取 [ 3 , 5 ] [3, 5] [3,5] 子段 { 3 , − 1 , 2 } \{3, -1, 2\} {3,1,2},其和为 4 4 4

数据规模与约定

  • 对于 40 % 40\% 40% 的数据,保证 n ≤ 2 × 1 0 3 n \leq 2 \times 10^3 n2×103
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 2 × 1 0 5 1 \leq n \leq 2 \times 10^5 1n2×105 − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 104ai104
#include<algorithm>
#include<iostream>
using namespace std;
int main()
{
    int n,temp,dp[200001],maxn=-999999999;
    cin>>n;
    for(int i=1;i<=n;i++)
    { 
        cin>>temp;
        //加上当前的数,和从当前这个数开始记,那个更大
        dp[i]=max(dp[i-1]+temp,temp);
        // 更新结果,这个必须有。因为dp[n]不是最后结果
        maxn=max(maxn,dp[i]);
    }
    cout<<maxn;
    return 0;
}

最大公共子序列问题

给出两个字符串例: asdfghj 和 asxcvfg
他们的公共子串就是 asf 长度为3,和子串不同,子序列不要求连续。

#include<iostream>
#include<string>
#define maxn 10001
using namespace std;
int dp[maxn][maxn];
int main()
{
    int n,maxlen=0,flag=0;
    string s1,s2;
    getline(cin,s1);
    getline(cin,s2);//输入两个字符串
    for(int i=0;i<s1.length();i++)
    {
        for(int j=0;j<s2.length();j++)
        {
            if(i==0||j==0)
            {
                if(s1[i]==s2[j])//特判一下,第一个字母和其他字母相同的,标志位加一
                    flag=1;
            }
            else
            {
                if(s1[i]==s2[j])
                    dp[i][j]=dp[i-1][j-1]+1;
                else
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
            maxlen=max(dp[i][j],maxlen);//更新当前最长子串的长度
            //cout<<maxlen<<" "<<dp[i][j]<<endl;
        }
    }
    cout<<maxlen+flag;
    return 0;
}

动态规划的问题,重点在于找到状态转移的方程

最长上升子序列

子序列,不要求连续

一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1 < i2 < … < iK≤N。比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。这些子序列中最长的长度是4,比如子序列(1,3,5,8)。 你的任务,就是对于给定的序列,求出最长上升子序列的长度。

格式

输入格式

输入的第一行是序列的长度N(1≤N≤1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。

输出格式

最长上升子序列的长度。

样例

输入样例

7
1 7 3 5 9 4 8
输出样例

4

在求以 n u m [ i ] num[i] num[i]末元素的最长递增子序列时,找到所有序号在 i i i前面且小于 n u m [ i ] num[i] num[i]的元素 n u m [ j ] num[j] num[j],即 j < i j<i j<i n u m [ j ] < n u m [ i ] num[j]<num[i] num[j]<num[i]。如果这样的元素存在,那么对所有 n u m [ j ] num[j] num[j],都有一个以 n u m [ j ] num[j] num[j]为末元素的最长递增子序列的长度 d p [ j ] dp[j] dp[j],把其中最大的 d p [ j ] dp[j] dp[j]选出来,那么 d p [ i ] dp[i] dp[i]就等于最大的 d p [ j ] + 1 dp[j]+1 dp[j]+1,即以 n u m [ i ] num[i] num[i]为末元素的最长递增子序列,等于以使 d p [ j ] dp[j] dp[j]最大的那个 n u m [ j ] num[j] num[j]为末元素的递增子序列最末再加上 n u m [ i ] num[i] num[i];如果这样的元素不存在,那么 n u m [ i ] num[i] num[i]自身构成一个长度为1的以 n u m [ i ] num[i] num[i]为末元素的递增子序列。

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=110;
int num[maxn],n,dp[maxn],res=-1;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>num[i];
    for(int i=1;i<=n;i++)
    {
        // 初始化,以自身结尾,长度为一的上升子序列
        dp[i]=1;
        for(int j=i-1;j>0;j--)
        {
            //找到nm[i]前面比num[i]小的数
            if(num[i]>num[j])
            {
                //比num[i]小的有很多,取最大长度的
                dp[i]=max(dp[j]+1,dp[i]);;
                res=max(res,dp[i]);
            }    
        }
    }
    cout<<res;
    return 0;
}
/*
7
1 7 3 5 9 4 8

4
*/

背包问题

背包九讲: https://github.com/tianyicui/pack

01背包

[NOIP2005 普及组] 采药

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 2 2 个整数 T T T 1 ≤ T ≤ 1000 1 \le T \le 1000 1T1000)和 M M M 1 ≤ M ≤ 100 1 \le M \le 100 1M100),用一个空格隔开, T T T 代表总共能够用来采药的时间, M M M 代表山洞里的草药的数目。接下来的 M M M 行每行包括两个在 1 1 1 100 100 100 之间(包括 1 1 1 100 100 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

输入输出样例

**输入 **

70 3
71 100
69 1
1 2

**输出 **

3

说明/提示

【数据范围】

  • 对于 30 % 30\% 30% 的数据, M ≤ 10 M \le 10 M10
  • 对于全部的数据, M ≤ 100 M \le 100 M100

【题目来源】

NOIP 2005 普及组第三题

// 01背包问题
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=110;
int w[maxn],value[maxn];
int t,m;
int dp[maxn*maxn];
int DP[maxn][maxn];
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>w[i]>>value[i];
        // 两种写法一种是一维dp数组
    for(int i=1;i<=m;i++)
    {
        // 内层循环要倒着来,防止一个物品放置多次
        for(int j=t;j>=0;j--)
        {
            // 当前剩余价值大于w[i]
            if(j>=w[i])
            // 对比放入i还是不放i
                dp[j]=max(dp[j-w[i]]+value[i],dp[j]);
        }
    }
    // 另一种二维dp数组,i代表第几种药,j代表当前剩余的空间
    for(int i=1;i<=m;i++)
    {
        for(int j=t;j>=0;j--)
        {
            if(j>=w[i])
                DP[i][j]=max(DP[i-1][j-w[i]]+value[i],DP[i-1][j]);
            else
                DP[i][j]=DP[i-1][j];
        }
    }
    // cout<<DP[m][t]<<endl;
    cout<<dp[t];
    return 0;
}

其他

P1002 [NOIP2002 普及组] 过河卒

题目描述

棋盘上 A A A 点有一个过河卒,需要走到目标 B B B 点。卒行走的规则:可以向下、或者向右。同时在棋盘上 C C C 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。

棋盘用坐标表示, A A A ( 0 , 0 ) (0, 0) (0,0) B B B ( n , m ) (n, m) (n,m),同样马的位置坐标是需要给出的。

img

现在要求你计算出卒从 A A A 点能够到达 B B B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。

输入格式

一行四个正整数,分别表示 B B B 点坐标和马的坐标。

输出格式

一个整数,表示所有的路径条数。

输入输出样例

输入

6 6 3 3

**输出 **

6

说明/提示

对于 100 % 100 \% 100% 的数据, 1 ≤ n , m ≤ 20 1 \le n, m \le 20 1n,m20 0 ≤ 0 \le 0 马的坐标 ≤ 20 \le 20 20

【题目来源】

NOIP 2002 普及组第四题

关键是找到状态转移方程此题当中就是dp[i][j]=dp[i-1][j]+dp[i][j-1];

到达(i,j)的路径条数等于(i-1,j)+(i,j-1)

#include<iostream>
using namespace std;
const int maxn=110;
bool b[maxn][maxn];//存放棋盘
long long dp[maxn][maxn];
int fx[8]={1,1,2,2,-1,-1,-2,-2};
int fy[8]={2,-2,1,-1,2,-2,1,-1};
int main()
{
    int endx,endy,mx,my;
    // 输入终点和马的坐标
    cin>>endx>>endy>>mx>>my;
    // 不能走的地方标记为 1
    b[mx][my]=1;
    for(int i=0;i<8;i++)
    {
        if(mx+fx[i]>=0&&my+fy[i]>=0&&mx+fx[i]<=endx&&my+fy[i]<=endy)
            b[mx+fx[i]][my+fy[i]]=1;
    }
    // 初始化dp数组,即,设置好基础状态
    int j=0,k=0;
    while (!b[k][0]&&k<=endx)
        dp[k++][0]=1;

    while (!b[0][j]&&j<=endy)
        dp[0][j++]=1;
    // 遍历图上的每个结点
    for(int i=1;i<=endx;i++)
    {
        for(int j=1;j<=endy;j++)
        {
            // 此点不能能走
            if(b[i][j])
                dp[i][j]=0;
            else//点(i,j)的路径条数,等于他前一步的路径次数相加,前一步包括了(i-1,j)和(i,j-1)
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
        }
    }
    cout<<dp[endx][endy];
    return 0;
}

P1130 红牌

某地临时居民想获得长期居住权就必须申请拿到红牌。获得红牌的过程是相当复杂 ,一共包括 N N N个步骤。每一步骤都由政府的某个工作人员负责检查你所提交的材料是否符合条件。为了加快进程,每一步政府都派了 M M M​个工作人员来检查材料。不幸的是,并不是每一个工作人员效率都很高。尽管如此,为了体现“公开政府”的政策,政府部门把每一个工作人员的处理一个申请所花天数都对外界公开。

为了防止所有申请人都到效率高的工作人员去申请。这 M × N M \times N M×N个工作人员被分成 M M M个小组。每一组在每一步都有一个工作人员。申请人可以选择任意一个小组也可以更换小组。但是更换小组是很严格的,一定要相邻两个步骤之间来更换,而不能在某一步骤已经开始但还没结束的时候提出更换,并且也只能从原来的小组I更换到小组 I + 1 I+1 I+1,当然从小组 M M M可以更换到小组 1 1 1。对更换小组的次数没有限制。

例如:下面是 3 3 3个小组,每个小组 4 4 4个步骤工作天数:

小组 1 1 1 : 2 , 6 , 1 , 8 2, 6 ,1 ,8 2,6,1,8

小组 2 2 2 : 3 , 6 , 2 , 6 3,6, 2, 6 3,6,2,6

小组 3 3 3 : $ 4, 2 ,3 ,6$

例子中,可以选择小组 1 1 1来完成整个过程一共花了 2 + 6 + 1 + 8 = 17 2+6+1+8=17 2+6+1+8=17天,也可以从小组 2 2 2开始第一步,然后第二步更换到小组3,第三步到小组 1 1 1,第四步再到小组 2 2 2,这样一共花了 3 + 2 + 1 + 6 = 12 3+2+1+6=12 3+2+1+6=12天。你可以发现没有比这样效率更高的选择

你的任务是求出完成申请所花最少天数。

输入输出格式

输入格式

第一行是两个正整数 N N N M M M,表示步数和小组数。接下来有 M M M行,每行 N N N个非负整数,第 i + 1 ( 1 ≤ i ≤ M ) i+1(1 \le i \le M) i+1(1iM)行的第j个数表示小组 i i i完成第j步所花的天数,天数都不超过 1000000 1000000 1000000

输出格式

一个正整数,为完成所有步所需最少天数

输入输出样例

**输入 **

4 3 2 6 1 83 6 2 64 2 3 6

**输出 **

12

【数据规模与约定】

对于 100 % 100\% 100%的数据,有 N ≤ 2000 , M ≤ 2000 N ≤ 2000, M ≤ 2000 N2000,M2000

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,dp[2005][2005],ans=1<<30;
int main()
{
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        for(int j=0;j<n;j++)
            cin>>dp[i][j];
    }
    // N个步骤
    for(int j=n-2;j>=0;j--)
    {
        // m个小组
        for(int i=0;i<m;i++)
        // 状态转移方程
            dp[i][j]=min(dp[(i+1)%m][j+1],dp[i][j+1])+dp[i][j];
    }
    for(int i=0;i<m;i++)
        ans=min(ans,dp[i][0]);
    cout<<ans;
    return 0;
}
#include<cstdio>
#include<algorithm>

const int maxn = 2005;
const int INF = 0x3f3f3f3f;

int n, m;
int a[maxn][maxn], f[maxn][maxn];

int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= m; ++i)
        for(int j = 1; j <= n; ++j)
            scanf("%d", &a[j][i]);
    
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
            f[i][j] = std::min(f[i - 1][j], j == 1 ? f[i - 1][m] : f[i - 1][j - 1]) + a[i][j];
    
    int ans = INF;
    for(int i = 1; i <= m; ++i)
        ans = std::min(ans, f[n][i]);
    printf("%d", ans);
    
    return 0;
}

f [ i ] [ j ] f[i][j] f[i][j] 表示第 i i i 阶段第 j j j 小组的最小天数转移方程为

f [ i ] [ j ] = min ⁡ ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − 1 ] ) + a [ i ] [ j ] f[i][j]=\min(f[i-1][j],f[i-1][j-1])+a[i][j] f[i][j]=min(f[i1][j],f[i1][j1])+a[i][j]

但是第 1 1 1 个小组最小天数是由第 m m m 个小组转移来的,所以需要多一个特判:

f [ i ] [ j ] = m i n ( f [ i − 1 ] [ j ] , j = = 1 ? f [ i − 1 ] [ m ] : f [ i − 1 ] [ j − 1 ] ) + a [ i ] [ j ] f[i][j]=min(f[i-1][j],j==1?f[i-1][m]:f[i-1][j-1])+a[i][j] f[i][j]=min(f[i1][j],j==1?f[i1][m]:f[i1][j1])+a[i][j]

还有一个比较恶心的问题,我们读入时是按第 i i i 小组第 j j j 阶段存储的,但转移时变成了第 i i i 阶段第 j j j 小组,所以可以稍微改变一下读入方式:

scanf("%d", &amp,&a[j][i]);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值