week7-动态规划(01背包)

1.[NOIP2005 普及组] 采药

题目描述

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

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

输入格式

第一行有 2 个整数 T(1 <= T <=1000)和 M(1 <= M <= 100),用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。

接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

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

样例 #1

样例输入 #1

70 3
71 100
69 1
1 2

样例输出 #1

3

提示

【数据范围】

  • 对于 30% 的数据,M <= 10;
  • 对于全部的数据,M <= 100。

【题目来源】

NOIP 2005 普及组第三题

这道题跟背包问题非常相似,背包问题的限制是物品的重量,而这个问题是采一株草药的时间,结果都是问怎样才能采到最大价值的东西,所以思路都是一样的,设dfs[i][j]为在i段时间内,j株草药内采到的草药的最大价值。那么状态转移方程为:

d f s [ i ] [ j ] = { d f s [ i ] [ j − 1 ] i > T m a x { d f s [ i − t i m e [ j ] ] [ j − 1 ] + v a l u e [ j ] , d f s [ i ] [ j − 1 ] } i < = T 0 i = 0 , j = 0 dfs[i][j]=\begin{cases} dfs[i][j-1] & i>T \\ max\{dfs[i-time[j]][j-1]+value[j],dfs[i][j-1]\} & i<=T \\ 0 & i=0,j=0 \end {cases} dfs[i][j]= dfs[i][j1]max{dfs[itime[j]][j1]+value[j],dfs[i][j1]}0i>Ti<=Ti=0,j=0
所以,根据公式,我们就能很快解决这个问题:

完整注释代码如下:

#include <bits/stdc++.h>
using namespace std;    
int T,M;
int t[105],value[105],ret[1005][105];   //这里用ret[i][j]记录dfs[i][j]的值,减小算法复杂度
int dfs(int i,int j){   //dfs(i,j)表示在i段时间内,j组草药中能采到草药的最大价值
    int ans;
    if(ret[i][j]!=-1){
        return ret[i][j];
    }
    if(i<=0||j==0){
        ans=0;
    }
    else if(t[j]>i){
        ans=dfs(i,j-1);
    }
    else{
        ans=max(dfs(i-t[j],j-1)+value[j],dfs(i,j-1));
    }
    ret[i][j]=ans;
    return ans;
}
int main(){
    cin>>T>>M;
    memset(ret,-1,sizeof(ret));
    for(int i=1;i<=M;i++){
        cin>>t[i]>>value[i];
    }
    cout<<dfs(T,M);
    return 0;
}

2.最长上升子序列

题目描述

这是一个简单的动规板子题。

给出一个由 n(n<= 5000) 个不超过 10^6 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。

最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。

输入格式

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

第二行有 n 个整数,表示这个序列。

输出格式

一个整数表示答案。

样例 #1

样例输入 #1

6
1 2 4 1 3 4

样例输出 #1

4

提示

分别取出 1、2、3、4 即可。

这道题肯定是可以用搜索来做的,直接爆搜,把每种方法都是一遍。但显然,这里的数据太大,是肯定会TLE的,所以,我们采取效率更高的动态规划来做~~(这不是废话吗?作业名不就叫动态规划吗?)~~。所以我们不妨设dfs[i]为以第i个数为结尾的最长上升子序列的长度,那么这里动态转移方程我们就能写出来:
d f s [ i ] = { m a x { d f s [ i ] , d f s [ j ] + 1 } n u m [ i ] > n u m [ j ] dfs[i]=\begin{cases} max\{dfs[i],dfs[j]+1\} & num[i]>num[j]\\ \end {cases} dfs[i]={max{dfs[i],dfs[j]+1}num[i]>num[j]
所以我们就很容易写出这道题的答案:

完整注释代码如下:

#include <bits/stdc++.h>
using namespace std;    
int main(){
    int n,num[5005],ans[5005],ret;   //用ans[i]表示dfs
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>num[i];
        ans[i]=1;   //注意初始条件为1,因为每个数都至少有自身这个最大上身子序列
    }
    for(int i=1;i<=n;i++){ 
        for(int j=1;j<=i;j++){
            if(num[j]<num[i]){
                ans[i]=max(ans[i],ans[j]+1);  
            }
        }
    }
    for(int i=1;i<=n;i++){
        ret=max(ret,ans[i]);   //找出最大的结果就是答案
    }
    cout<<ret;
    return 0;
}

3.最大子段和

题目描述

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

输入格式

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

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

输出格式

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

样例 #1

样例输入 #1

7
2 -4 3 -1 2 -4 3

样例输出 #1

4

提示

样例 1 解释

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

数据规模与约定

  • 对于 40% 的数据,保证 n <= 2 * 10^3。
  • 对于 100% 的数据,保证 1 <= n <= 2 * 105,-104 <= a_i <= 10^4。

这道题我有一种好的想法:我们可以从头遍历到尾,用一个sum表示从第一个数加到第i个数的和,再定义一个max记录当前sum的最大值(sum初始化为0,max初始化为第一个数的值),那么我们就可以很快发现如果sum加完上一个数后如果小于0,那么我们就应该抛弃前面的所以数的值,再初始化sum为0(原因也很简单,因为前面的这些数的和是负数,只会让后面的数的和变小,那么当然要果断抛弃他们太可怜了qwq),当sum加完后依旧大于0,就代表前面的这些数之和还是可以让后面的这些数变大的,所以要继续向下加,每次加,我们都要用max记录sum的最大值,直到加到尾。那么这是的max也就是结果了.(其实也包含了动态规划的思想在里面)

完整注释代码如下:

#include <bits/stdc++.h>
using namespace std;    
int main(){
    int n,num[200005],sum;
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>num[i];
    }        
    sum=0;
    mx=num[0];
    for(int i=0;i<n;i++){
        sum+=num[i];
        if(sum>=0){
            mx=max(sum,mx);  //如果sum>=0,那么就继续加下去,并记录最大值
        }
        else if(sum<0){
            mx=max(sum,mx);  //如果小于0就要果断抛弃qwq,并记录最大值
            sum=0;
        }
    }
    cout<<mx;
    return 0;
}

4.LCS

题面翻译

题目描述:

给定一个字符串 s和一个字符串 t ,输出 s 和 t 的最长公共子序列。

输入格式:

两行,第一行输入 s ,第二行输入 t 。

输出格式:

输出 s 和 t的最长公共子序列。如果有多种答案,输出任何一个都可以。

说明/提示:

数据保证 s 和 t 仅含英文小写字母,并且 s 和 t 的长度小于等于3000。

题目描述

文字列 $ s $ および $ t $ が与えられます。 $ s $ の部分列かつ $ t $ の部分列であるような文字列のうち、最長のものをひとつ求めてください。

输入格式

入力は以下の形式で標準入力から与えられる。

$ s $ $ t $

输出格式

$ s $ の部分列かつ $ t $ の部分列であるような文字列のうち、最長のものをひとつ出力せよ。 答えが複数ある場合、どれを出力してもよい。

样例 #1

样例输入 #1

axyb
abyxb

样例输出 #1

axb

样例 #2

样例输入 #2

aa
xayaz

样例输出 #2

aa

样例 #3

样例输入 #3

a
z

样例输出 #3


样例 #4

样例输入 #4

abracadabra
avadakedavra

样例输出 #4

aaadara

提示

注釈

文字列 $ x $ の部分列とは、$ x $ から $ 0 $ 個以上の文字を取り除いた後、残りの文字を元の順序で連結して得られる文字列のことです。

制約

  • $ s $ および $ t $ は英小文字からなる文字列である。
  • $ 1\ \leq\ |s|,\ |t|\ \leq\ 3000 $

Sample Explanation 1

答えは axb または ayb です。 どちらを出力しても正解となります。

Sample Explanation 3

答えは `` (空文字列) です。

这道题我一开始是完全想不出什么思路,直到我灵感突然迸发~~(看了题解后),这了我就引用洛谷这位大佬的题解来解释一下(其实就是我看的人的题解)~~

证明的步骤直接查看题解的步骤就行了(我实在是语言贫瘠),所以我直接来看这个状态转移方程:

(这里用C[i][j]表示在a的i个字符长度和b的j个字符长度中的最大LCS的长度)
C [ i , j ] = { 0 若 i = 0 或 j = 0 C [ i − 1 , j − 1 ] + 1 若 i , j > 0 且 x i = y j max ⁡ ( C [ i , j − 1 ] , C [ i − 1 , j ] ) 若 i , j > 0 且 x i ≠ y j C[i,j]=\begin{cases} 0 & \text{若}i=0\text{或}j=0\\ C[i-1,j-1]+1 & \text{若}i,j>0\text{且}x_i=y_j\\ \max(C[i,j-1],C[i-1,j]) & \text{若}i,j>0\text{且}x_i\not=y_j \end{cases} C[i,j]= 0C[i1,j1]+1max(C[i,j1],C[i1,j])i=0j=0i,j>0xi=yji,j>0xi=yj
其实这个就是LCS的结论的公式表达

  1. 若x_n=y_mx**n=y**m,则x_n=y_m=z_kx**n=y**m=z**k且Z_{k-1}Z**k−1是X_{n-1}X**n−1和Y_{m-1}Y**m−1的LCS。
  2. 若x_n\not=y_mx**n\=y**m,当z_k\not=x_nz**k\=x**n时,ZZ是X_{n-1}X**n−1和YY的LCS。
  3. 若x_n\not=y_mx**n\=y**m,当z_k\not=y_mz**k\=y**m时,ZZ是XX和Y_{m-1}Y**m−1的LCS。

我们重在理解这个过程.

此外,题目要求我们输出这个子序列,所以在每一次做选择的时候,要记下来是从哪里得到这个答案的。可以用一个pre[i,j]pre[i,j]指向C[i,j]C[i,j]得到答案是是从哪里选择的,详细的我会在代码注释中解释。

完整注释代码如下:

#include <iostream>
using namespace std;
const int maxn = 3005;
int dp[maxn][maxn],pre[maxn][maxn];    //dp[i][j]用于记录结果
string A,B;
int solve(int a_now,int b_now){
	if(a_now<0 || b_now<0){
        return 0;  //如果长度有一个为0,那么长度必为0
    }
	if(dp[a_now][b_now]){
        return dp[a_now][b_now];  //如果已经算过就返回结果
    }
	if(A[a_now] == B[b_now]){  
		pre[a_now][b_now]=1;  //1表示要找的字符就在(a_now,b_now)的位置上,并需要继续追踪(a_now-1,b_now-1)的位置
        dp[a_now][b_now]=solve(a_now-1,b_now-1)+1; 
		return dp[a_now][b_now];
	} else {
		int x1 = solve(a_now-1,b_now);
		int x2 = solve(a_now,b_now-1);
		if(x1 > x2){
			pre[a_now][b_now]=2;  //2表示要追踪的字符在(a_now-1,b_now)的位置上
            dp[a_now][b_now]=x1;  
			return x1;
		} 
        else {
			pre[a_now][b_now]=3;   //3表示要追踪的字符在(a_now,b_now-1)的位置上
            dp[a_now][b_now]=x2;
			return x2;
		}
	}
}
void print(int x,int y){  //反向追踪字符
	if(x<0 || y<0){  //如果到0就返回
        return;
    }
	if(pre[x][y] == 1){  
		print(x-1,y-1);
        cout<<A[x];
	} 
    else if(pre[x][y] == 2){
        print(x-1,y);
    }
	else {
        print(x,y-1);
    }
}

int main(){
	cin>>A>>B;
    int len1=A.length(),len2=B.length();
	solve(len1,len2);
	print(len1,len2);
	return 0;
}

这道题太难了,不会做qwq

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值