【动态规划1】动态规划的引入 练习笔记

【动态规划1】动态规划的引入

题目链接:https://www.luogu.com.cn/training/211#problems

T1 数字三角形 Number Triangles

题目描述

观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

 		7 
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

在上面的样例中,从 7 ->3 -> 8 -> 7 -> 5 的路径产生了最大

输入格式
第一个行一个正整数 rr ,表示行的数目。

后面每行为这个数字金字塔特定行包含的整数。

输出格式
单独的一行,包含那个可能得到的最大的和。

输入输出样例
输入

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5 

输出

30

说明/提示
【数据范围】
对于 100% 的数据,1≤r≤1000,所有输入在 [0,100] 范围内。

题目翻译来自NOCOW。

USACO Training Section 1.5

IOI1994 Day1T1

解题笔记

分析可知:

  • 用dp[i][j]表示从底部到(i,j)的最长路径,用d[i][j]存储(i,j)位置上的数字,则答案就是dp[1][1]
  • dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+d[i][j]
记忆化搜索写法

注意dp数组一定要初始化为负数

#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e3+5;
const int INF = 0x7fffffff;

int r;
int d[MAX][MAX];
int dp[MAX][MAX];

int dfs(int i,int j){
    if(dp[i][j]>-1) return dp[i][j];
    if(i==r) return dp[i][j] = d[i][j];
    return dp[i][j] = max(dfs(i+1,j),dfs(i+1,j+1))+d[i][j];

}

int main()
{
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL

    scanf("%d",&r);
    for(int i=1;i<=r;i++){
        for(int j=1;j<=i;j++) {
            scanf("%d",&d[i][j]);
        }
    }

    memset(dp,-1,sizeof(dp)); //关键代码,要将dp数组的每一个值初始化为负数,否则当d数组每个位置上都是0时会爆栈。
    printf("%d\n",dfs(1,1));

    return 0;
}

动态规划写法
#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e3+5;
const int INF = 0x7fffffff;

int r;
int d[MAX][MAX];
int dp[MAX][MAX];

int main()
{
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL

    scanf("%d",&r);
    for(int i=1;i<=r;i++){
        for(int j=1;j<=i;j++) {
            scanf("%d",&d[i][j]);
        }
    }

    for(int i=1;i<=r;i++) dp[r][i]=d[r][i]; //初始化操作
    for(int i=r-1;i>=1;i--){//一定是倒推
        for(int j=1;j<=i;j++){
            dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+d[i][j];
        }
    }
    printf("%d\n",dp[1][1]);

    return 0;
}

T2 P1434 [SHOI2002]滑雪

题目描述

Michael 喜欢滑雪。这并不奇怪,因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael 想知道在一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:

1   2   3   4   5
16  17  18  19  6
15  24  25  20  7
14  23  22  21  8
13  12  11  10  9

一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度会减小。在上面的例子中,一条可行的滑坡为 24-17-16-11(从 24 开始,在 11 结束)。当然 25-24-23-…-33-22-11 更长。事实上,这是最长的一条。

输入格式
输入的第一行为表示区域的二维数组的行数 R和列数C。下面是 R 行,每行有 C 个数,代表高度(两个数字之间用 1个空格间隔)。

输出格式
输出区域中最长滑坡的长度。

输入输出样例
输入

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出

25

说明/提示
对于 100% 的数据,1001≤R,C≤100。

解题笔记

注意:题中的长度是指经过的点的个数,并非高度差

记忆化搜索写法
#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e2+5;
const int INF = 0x7fffffff;

int R,C;
int d[MAX][MAX];
int dp[MAX][MAX];

int dir[4][2]={{1,0},{0,1},{-1,0},{0,-1}};

int dfs(int i,int j){
    if(dp[i][j]) return dp[i][j];//搜过直接返回
    for(int k=0;k<4;k++){
        int ni=i+dir[k][0];
        int nj=j+dir[k][1];
        if(ni<1||nj<1||ni>R||nj>C) continue;
        if(d[i][j]<=d[ni][nj]) continue;
        dp[i][j] = max(dfs(ni,nj)+1,dp[i][j]); //状态转移方程
    }
    if(dp[i][j]==0) dp[i][j]=1; //如果相邻的点均大于等于自身高度,则只会经过(i,j)一个点
    return dp[i][j];
}

int main()
{
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL
    scanf("%d%d",&R,&C);
    for (int i=1;i<=R;i++){
        for (int j=1;j<=C;j++){
            scanf("%d",&d[i][j]);
        }
    }
    memset(dp,0,sizeof(dp));
    int res = 0;
    for(int i=1;i<=R;i++){
        for(int j=1;j<=C;j++){
            res = max(res,dfs(i,j));
        }
    }
    printf("%d\n",res);
    return 0;
}

DP写法

学完线性dp 有机会再写

T3 P2196 [NOIP1996 提高组] 挖地雷

题目描述

在一个地图上有N个地窖(N≤20),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。

输入格式

有若干行。

第1行只有一个数字,表示地窖的个数N。

第2行有N个数,分别表示每个地窖中的地雷个数。

第3行至第N+1行表示地窖之间的连接情况:

第3行有n−1个数(0或1),表示第一个地窖至第2个、第3个、…、第n个地窖有否路径连接。如第3行为 011000…0,则表示第1个地窖至第2个地窖有路径,至第3个地窖有路径,至第4个地窖、第5个、…、第n个地窖没有路径。

第4行有n−2个数,表示第二个地窖至第3个、第4个、…、第n个地窖有否路径连接。

… …

第n+1行有1个数,表示第n−1个地窖至第n个地窖有否路径连接。(为0表示没有路径,为1表示有路径)。

输出格式

有两行

第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。

第二行只有一个数,表示能挖到的最多地雷数。

输入输出样例

输入

5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1

输出

1 3 4 5
27

解题笔记

这道题可以拆成两个问题:

  1. 求挖到的地雷的最大个数
  2. 求能挖到最大个数地雷的路径
  • 对与第一个问题:可以容易的分析得到状态转移方程
    dp[i]=max(dp[j)+nums[i]其中dp[i]表示从i开始挖,能挖到的地雷的最大个数,同时<i,j>有路径,nums[i],表示i位置上的地雷个数

  • 对于第二个问题:可以在求最大地雷的过程中,维护一个数组path,path[i]表示要想从i开始挖到最大个数的地雷,下一个地点为path[i].

记忆化搜索写法
#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e2+5;
const int INF = 0x7fffffff;

int N;
int nums[MAX];
int maps[MAX][MAX];
int dp[MAX];

int path[MAX];

int dfs(int index){ //记忆化搜索
    if(dp[index]>-1) return dp[index];
    for(int i=index+1;i<=N;i++){
        if(maps[index][i]) {
            if(dp[index]<dfs(i)+nums[index]){
                dp[index]=dfs(i)+nums[index];
                path[index]=i;
            }
        }
    }
    if(dp[index]==-1) {
        path[index]=index;
        return dp[index] = nums[index];
    }
    return dp[index];
}

void printPath(int bg){ //打印挖地雷的路径
    printf("%d",bg);
    while(1){
        if(bg==path[bg]) break;
        bg = path[bg];
        printf(" %d",bg);

    }
    printf("\n");

}

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL
    scanf("%d",&N);
    for(int i=1;i<=N;i++) scanf("%d",&nums[i]);

    for(int i=1;i<N;i++){
        for (int j=i+1;j<=N;j++) {
            scanf("%d",&maps[i][j]);
        }
    }

    memset(dp,-1,sizeof(dp));

    for(int i=1;i<=N;i++) path[i]=i;//路径初始化

    int res = 0;
    int bg = -1;
    for(int i=1;i<=N;i++){//寻找挖地雷的开始位置
        if(res < dfs(i)) {
            bg = i;
            res = dfs(i);
        }
    }
    printPath(bg);


    printf("%d\n",res);

    return 0;
}

T4 P4017 最大食物链计数

题目背景

你知道食物链吗?Delia 生物考试的时候,数食物链条数的题目全都错了,因为她总是重复数了几条或漏掉了几条。于是她来就来求助你,然而你也不会啊!写一个程序来帮帮她吧

题目描述

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有 1 秒的时间。

由于这个结果可能过大,你只需要输出总数模上 80112002 的结果。

输入格式

第一行,两个正整数 n、m,表示生物种类 n 和吃与被吃的关系数 m。

接下来 m 行,每行两个正整数,表示被吃的生物A和吃A的生物B。

输出格式

一行一个整数,为最大食物链数量模上 80112002的结果

输入输出样例

输入

5 7
1 2
1 3
2 3
3 5
2 5
4 5
3 4

输出

5

解题笔记

这道题就是一道简单的递推。很容易得到状态转移方程
dp[i]=sum(dp[j])%80112002 其中dp[i]为从i作为捕食者源头的食物链个数,其中j可以被j捕食

记忆化搜索代码如下
#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 5e3+5;
const int INF = 0x7fffffff;
const int MOD = 80112002;

int m,n;

bool relation[MAX][MAX];
int countIn[MAX];
int countOut[MAX];
int dp[MAX];

int dfs(int index){
    if(dp[index]) return dp[index];
    if(countOut[index]==0) return dp[index]=1; //食物链底端
    int ans=0;
    for(int i=1;i<=n;i++) {
        if(relation[index][i]) {
            ans = (ans + dfs(i))%MOD;
        }
    }
    return dp[index]=ans;
}

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL

    scanf("%d%d",&n,&m);
    while(m--){
        int x,y;
        scanf("%d%d",&x,&y);
        relation[y][x]=true;
        countIn[x]++; //天敌个数
        countOut[y]++;//猎物个数
    }
    int res = 0;
    for(int i=1;i<=n;i++){
        if(countIn[i]==0) {//每条食物链顶端的生物没有天敌
            res=(res+dfs(i))%MOD;
        }
    }
    printf("%d\n",res);
    return 0;
}

T5 P1048 [NOIP2005 普及组] 采药

题目描述

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

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

输入格式

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

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

输出格式

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

输入输出样例

输入

70 3
71 100
69 1
1 2

输出

3

说明/提示

【数据范围】

对于 30% 的数据M≤10;
对于全部的数据,M≤100。

解题笔记

首先分析每个药的状态,只有两种,采或不采。对于从左往右依次检查每个药的状态时。得到状态转移方程:
dp[index][leftTiime]=max(dp[index+1][leftTime-costTime[index]]+value[index],dp[index+1][leftTime])

首先采用爆搜来写,结果只有30分。接下来可以考虑到记忆化搜素,用二维数组记录dp[index][leftTime].

记忆化搜索代码如下
#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e2+5;
const int INF = 0x7fffffff;

int T,M;
int t[MAX],v[MAX];
int dp[MAX][MAX*10];

int dfs(int index,int leftTime){
    if(dp[index][leftTime]>-1) return dp[index][leftTime];
    if(index==M) return dp[index][leftTime]=0;
    if(t[index]>leftTime) return dp[index][leftTime]=dfs(index+1,leftTime);
    return dp[index][leftTime]=max(dfs(index+1,leftTime-t[index])+v[index],dfs(index+1,leftTime));
}

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL
    memset(dp,-1,sizeof(dp));
    scanf("%d%d",&T,&M);
    for(int i=0;i<M;i++){
        scanf("%d%d",&t[i],&v[i]);
    }
    printf("%d\n",dfs(0,T));

    return 0;
}

01背包写法

典型的01背包模板,直接背模板即可

#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e4+5;
const int INF = 0x7fffffff;

int T,M;
int t[MAX],v[MAX];

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL

    scanf("%d%d",&T,&M);
    for(int i=1;i<=M;i++){
        scanf("%d%d",&t[i],&v[i]);
    }
    int dp[T+5];
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=M;i++){
        for(int j=T;j>=t[i];j--){
            dp[j]=max(dp[j],dp[j-t[i]]+v[i]);
        }
    }
    printf("%d\n",dp[T]);

    return 0;
}

T6 P1616 疯狂的采药

题目描述

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

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

此题和原题的不同点:

  1. 每种草药可以无限制地疯狂采摘。

  2. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!

输入格式

输入第一行有两个整数,分别代表总共能够用来采药的时间 tt 和代表山洞里的草药的数目 mm。

第 2 到第 (m+1) 行,每行两个整数,第 (i+1) 行的整数 a_i, b_ia 分别表示采摘第 ii 种草药的时间和该草药的价值。

输出格式

输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

输入输出样例

输入
70 3
71 100
69 1
1 2
输出
140

说明/提示

数据规模与约定
对于30% 的数据,保证 m<= 10^3
对于100% 的数据,保证 1≤m≤104 ,1≤t≤107 ,且 1≤m×t≤10 7 ,1≤ai,bi≤104

解题笔记

首先尝试使用记忆化搜索写法,结果发现二维数组无法完整的保存。记忆化搜索又不方便压缩成一维的,之好放弃记忆化搜索写法,改用压缩后的一位数组书写动态规划算法。dp写法如下,注意 要用long long int保存结果

#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int MAX = 1e4+5;
const int INF = 0x7fffffff;

int T,M;
int t[MAX],v[MAX];

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL

    scanf("%d%d",&T,&M);
    for(int i=1;i<=M;i++){
        scanf("%d%d",&t[i],&v[i]);
    }
    ll dp[T+5];
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=M;i++){
        for(int j=t[i];j<=T;j++){
            dp[j]=max(dp[j],dp[j-t[i]]+v[i]);
        }
    }
    printf("%lld\n",dp[T]);

    return 0;
}

T7 P1802 5倍经验日

题目描述

现在absi2011拿出了x个迷你装药物(嗑药打人可耻….),准备开始与那些人打了

由于迷你装一个只能管一次,所以absi2011要谨慎的使用这些药,悲剧的是,没到达最少打败该人所用的属性药了他打人必输>.<所以他用2个药去打别人,别人却表明3个药才能打过,那么相当于你输了并且这两个属性药浪费了。

现在有n个好友,有输掉拿的经验、赢了拿的经验、要嗑几个药才能打过。求出最大经验(注意,最后要乘以5)

输入格式

第一行两个数,n和x

后面n行每行三个数,分别表示输了拿到的经验(lose[i])、赢了拿到的经验(win[i])、打过要至少使用的药数量(use[i])。

输出格式

一个整数,最多获得的经验

输入输出样例

输入
6 8
21 52 1
21 70 5
21 48 2
14 38 3
14 36 1
14 36 2
输出
1060

说明/提示

【Hint】

五倍经验活动的时候,absi2011总是吃体力药水而不是这种属性药>.<

【数据范围】

对于10%的数据,保证x=0

对于30%的数据,保证n<=10,x<=20

对于60%的数据,保证n<=100,x<=100, 10<=lose[i], win[i]<=100,use[i]<=5

对于100%的数据,保证n<=1000,x<=1000,0<lose[i]<=win[i]<=1000000,0<=use[i]<=1000

解题笔记

题目感觉没讲清楚(当然也有可能是我的理解问题有问题):是否可以不装药就可以跟别人打?
事实证明:题目隐藏了这一条件:不装药直接认输,就可以拿到lose的经验值。
仔细思考就可以知道:这是一个01背包的变形题。有两种状态,打得过别人/打不过别人。
状态转移方程:dp[v]=max(dp[v]+lose[i],dp[v-use[i]]+win[i]);
进一步分析:当剩下的药<use[i]时,dp[v]=dp[v]+lose[i],即转移方程的后半部分不存在。只有当剩下 的药>use[i]时,才需要判断是否要花费足够的药赢下比赛。
代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int MAX = 1e3+5;
const int INF = 0x7fffffff;

int n,x;
int lose[MAX],win[MAX],use[MAX];

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL
    scanf("%d%d",&n,&x);
    int res = 0;
    for(int i=1;i<=n;i++){
        scanf("%d%d%d",&lose[i],&win[i],&use[i]);
        if(use[i]==0) res+=win[i];
    }

    int dp[x+5];
    memset(dp,0,sizeof(dp));

    for(int i=1;i<=n;i++){
        for(int j=x;j>=use[i];j--){
            dp[j]=max(dp[j]+lose[i],dp[j-use[i]]+win[i]);
        }
        for(int j=use[i]-1;j>=0;j--){
            dp[j]+=lose[i];
        }
    }
    printf("%lld\n",5ll*dp[x]);//注意是5倍经验,最后结果还有可能爆int

    return 0;
}

T8 P1002 [NOIP2002 普及组] 过河卒

题目描述

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

棋盘用坐标表示,A 点(0,0)、B 点(n,m),同样马的位置坐标是需要给出的。
在这里插入图片描述
现在要求你计算出卒从 A 点能够到达 B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。

输入格式

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

输出格式

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

输入输出样例

输入
6 6 3 3
输出
6

说明/提示

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

解题笔记

对于每一个可达的点(i,j),它总是从(i-1,j)或者(i,j-1)两点一步走过来的,故可以使用状态转移方程进行描述:
1dp[i][j]=dp[i-1][j]+dp[i][j-1]
其中dp[i][j]表示从(0,0)走到(i,j)的路径个数.由于存在多个马的控制点。故在进行计算dp[i][j]时需要对控制点进行处理,详见代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int MAX = 20+5;
const int INF = 0x7fffffff;

struct Point{
    int x,y;
};
Point m,b;

int dir[8][2]={{-1,-2},{-2,-1},{-2,1},{-1,2},{1,2},{2,1},{2,-1},{1,-2}};
int path[MAX][MAX];
ll dp[MAX][MAX];

bool check(int x,int y){
    if(x<0||y<0||x>b.x||y>b.y) return false;
    return true;
}

int main() {
#ifdef LOCAL
    freopen("input","r",stdin);
#endif // LOCAL

    scanf("%d%d%d%d",&b.x,&b.y,&m.x,&m.y);
    //地图初始化
    path[m.x][m.y]=1;
    for(int i=0;i<8;i++){
        if(check(m.x+dir[i][0],m.y+dir[i][1]))
            path[m.x+dir[i][0]][m.y+dir[i][1]]=1;
    }
    //第0列初始化
    for(int i=0;i<=b.x;i++) {
        if(check(i,0)&&!path[i][0]) dp[i][0]=1;
        else break;
    }
     //第0行初始化
    for(int i=0;i<=b.y;i++) {
        if(check(0,i)&&!path[0][i]) dp[0][i]=1;
        else break;
    }

    for(int i=1;i<=b.x;i++) {
        for(int j=1;j<=b.y;j++){
            if(path[i][j]) continue; //当前点不可达,要保持0
            if(dp[i-1][j]!=0) //上面有路径
                dp[i][j]+=dp[i-1][j];
            if(dp[i][j-1]!=0) //左边有路径
                dp[i][j]+=dp[i][j-1];
        }
    }

    printf("%lld\n",dp[b.x][b.y]); //最后结果可能爆int

    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值