【动态规划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
解题笔记
这道题可以拆成两个问题:
- 求挖到的地雷的最大个数
- 求能挖到最大个数地雷的路径
-
对与第一个问题:可以容易的分析得到状态转移方程
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,你能完成这个任务吗?
此题和原题的不同点:
-
每种草药可以无限制地疯狂采摘。
-
药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 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;
}