入门DP教程(超详细)

参考题目:
1.1492: Problem D
2.01背包
3.简单题
4.做功课


一.理解DP的核心思想

案例:1492: Problem D
题目:Chieh最近在网上看到蓝翔非常火热。不知为什么他想到了一个问题,有一个n*m的矩阵每一个小块里有k个石头。。现在挖掘机在左上角,挖掘机非常强大。。可以放无限的石头但是它只能往右或者往下移动,现在Chieh想知道最多的石头从左上角到右下角。
T 组数 T<=100,n m 1<=n. m<=1000,n行m个数 为石头数量,0<=s<=1000
Sample Input
2
1 1
1
4 3
1 3 4
11 32 4
11 32 11
44 21 41
Sample Output
1
138

1.思考

题目的路径只能从左上往右下,如果用DFS跑图也许会TLE,那么只能引用到一种新的思想:每走一步都是最优解,那么这样最优到头,就不存在像DFS一样回头找其它路径然后比对的过程了。为了实现每走一步都是最优解,我们必须把一些数据进行预先比对,然后挑出最优解。就像这题所说的,我们可以列个表,把图画下来更容易理解。

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

在i=0和j=0这一行列里,因为不需要考虑,所以都标记为0,而其它的都是一些随机的数字当作石头数量,我们要做的,就是从左上角吃到右下角。那么每次移动都要考虑当前是否为最优解,即要和先前一步的数据(即(i-1,j)和(i,j-1)比较大小),由于需要存储每一步之后的石头数量总和,我们需要开一个二维数组去记录每一次判断发现的当时位置的石头数量最大值从而才能比较择优(这个数组是动态的,也就是说,它的每一个位置都取决于那个位置和在它之前遍历到的位置的石头数量,有可能是数字的累加,也有可能是择优)。那么,相当于我们择优时只要对它之前一步的位置的最优数组的判断和赋值,然后最后输出就好了。

//dp数组来存储当前位置可达到的最大值,a数组来存储每格的石头数量
 dp[i][j] = max(dp[i-1][j], dp[i][j-1])+a[i][j];

而这就是DP的核心代码(动态规划方程)。

2.完整代码

有了核心代码(动态规划方程),我们再跑图和其它基本操作就完事了

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1005;
#define ll long long
int a[maxn][maxn],dp[maxn][maxn];
int main(){
    
    
    int n,m;
    int T;
    cin >> T;
    while (T--) {
    
    
        memset(dp, 0, sizeof dp);
        memset(a, 0, sizeof a);//清零
        scanf("%d%d",&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] = max(dp[i-1][j], dp[i][j-1])+a[i][j];
            }
        }//跑图
        cout << dp[n][m] << endl;//输出
    }
    return 0;
    }

因此,在做DP时只要写出它的核心代码(动态规划方程)就相当于干出了一半了。

二.其它二维DP的改写

从1里面知道了DP是怎样运作的之后,对于其它简单的题目便可以快速规划了。
案例:01背包
题目很经典,就是好多不一样重量和价值的物品要放入一个体积不变的背包,求最多放多少价值。

1.核心代码的实现

在这种背包问题里,我们可以把动态数组的值当作是背包的总价值,X轴当作背包最大重量,Y轴当作物品最多数量,那么就和上一题一样的思想了:找到核心动态规划方程。当前有一个要考虑的情况:背包如果再装一个物品就会溢出,那么必须后退一步。因此,加一个if就完事了。

for(int i=1;i<=n;i++){
    
    
        for(int j=1;j<=C;j++){
    
    
            if(j<weight[i-1]){
    
      //包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的
			V[i][j]=V[i-1][j];
	        }else{
    
      //还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个
                V[i][j]=max(V[i-1][j],V[i-1][j-weight[i-1]]+value[i-1]);		
			}
		}
	}

2.完整代码

#include<bits/stdc++.h>
using namespace std;
int V[100][100];//前i个物品装入容量为j的背包中获得的最大价值
int KnapSack(int n,int weight[],int value[],int C){
    
    
    for(int i=0;i<=n;i++){
    
    
        V[i][0]=0;
    }
    for(int j=0;j<=C;j++){
    
    
        V[0][j]=0;
    }//填表,其中第一行和第一列全为0,即 V(i,0)=V(0,j)=0; 
    //用到的矩阵部分V[n][C] ,下面输出中并不输出 第1行和第1列 
	for(int i=1;i<=C;i++)   
for(int i=1;i<=n;i++){
    
    
        for(int j=1;j<=C;j++){
    
    
            if(j<weight[i-1]){
    
      //包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的
			V[i][j]=V[i-1][j];
	        }else{
    
      //还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个
            V[i][j]=max(V[i-1][j],V[i-1][j-weight[i-1]]+value[i-1]);		
		}
	}
}
        return V[n][C];
}
int main(){
    
    
    int n;        //物品数量 
    int Capacity;//背包最大容量
    scanf("%d",&Capacity);//请输入背包的最大容量
    scanf("%d",&n);//输入物品数
    for(int i=0;i<n;i++)
        scanf("%d",&weight[i]);//请分别输入物品的重量
    for(int i=0;i<n;i++)
        scanf("%d",&value[i]);//分别输入物品的价值
    int s=KnapSack(n,weight,value,Capacity);  //获得的最大价值
    printf("%d\n",s);//最大物品价值为
    return 0;
}

三.其它一维DP的改写(1)

从2里面知道了DP题目是怎样规划方程之后,对于有一些线性的题目就可以采用其它方法来解决。
案例:简单题
题目:这里有不同重量的砝码 可以是1g,2g。。。现在给你一个数N表示有N种重量的砝码 ai。。。an表示重量 bi。。。bn表示数量 问你不能称量出最少几克的重量 (最大不超过8500克哦亲)
第一行输入N表示砝码的重量种类(N=0结束)
接下来N行每行输入ai ,bi表示砝码的质量和数量(a<100,b<100)
输出不能称量出的质量中最少的质量
Sample Input
3
1 1
2 1
5 3
3
1 1
2 1
3 1
Sample Output
4
7

1.核心代码的实现

我们可以把砝码总重量的变动作为一个for循环,这样开一个一维数组就可以了,然后dp值依旧是当前砝码规律的重量,其次,将输入的砝码数量按照其重量拆分并存入一个一维数组,如:a[2]=5 代表一个编号为2,质量为5的砝码(标号与其重量的砝码数量无关)

dp[j]=max(dp[j],dp[j-a[i]]+a[i]);//要么不放,要么放

2.完整代码

遍历的循环是砝码数量和总重量,然后再修修补补

#include<bits/stdc++.h>
using namespace std;
int dp[10010];
int a[100010];
int q[100010];
int main(){
    
    
    int n;
    while(cin>>n){
    
    
        if(n==0)break;
        int sum=0;
        int k=0;
        int x,y;
        for(int i=0;i<n;i++){
    
    
        	cin>>x>>y;
            sum+=x*y;//统计总重量
            while(y!=0){
    
    
                a[k++]=x;//重新标号
                y--;
            }
        }
        sort(a,a+k);//排序
        memset(q,0,sizeof(q));
        memset(dp,0,sizeof(dp));
        for(int i=0;i<k;i++){
    
    //砝码总共数量
            for(int j=sum;j>=a[i];j--){
    
    //一单位一单位的减少,直到当前最小的那个用完(因为前面已经sort了)
                dp[j]=max(dp[j],dp[j-a[i]]+a[i]);
                q[dp[j]]=1;
            }
        }
        for(int i=1;i<=8500;i++){
    
    
            if(q[i]==0){
    
      
                cout<<i<<endl;
                break;
            }
        }//遍历,找出最小值
    }
    return 0;
}

三.其它一维DP的改写(2)

其实还可以用结构体来解决。
案例:做功课
题目:伊格内修斯刚刚参加完ACM/ ICPC比赛回来。现在,他有很多功课要做。每一位老师给他规定了功课提交的最后期限。
如果伊格内修斯的在截止日期后的递交作业,老师会减少他的最终测试得分。现在我们假设他每天都在做功
课。每门功课总是需要一天时间。现在,伊格请你帮他安排做功课,尽量减少降低分数的顺序。
输入包含多个测试用例。输入的第一行是一个单一的整数t即测试用例的数目。
每个测试用例开始一个正整数N(1<= N <= 1000),其表示一共有多少门作业作业。接着两行数据。
第一行包含N个整数,表明该科目的最后期限,而下一行包含N个整数,表明了降低分数。
对于每一个例子,你需要输出最少的可能降低的分数,每个测试占一行
Sample Input
3
3
3 3 3
10 5 1
3
1 3 1
6 2 3
7
1 4 6 4 2 4 3
3 2 1 7 6 5 4
Sample Output
0
3
5

1.核心代码的实现

f代表最大分数,要么写,要么不写

f[j]=max(f[j],f[j-1]+a[i].score);//求第j天能获得的最大分数

2.完整代码

遍历的循环是功课总数量和当前功课所需时间,然后再修修补补

#include <bits/stdc++.h>
using namespace std;
struct E{
    
    
    int time;
    int score;
}a[1001];
bool cmp(E b,E c){
    
    
    if(b.time!=c.time)
        return b.time<c.time;
    return b.score>c.score;
}
int f[100010];
int main(){
    
    
    int t,i,j,n,sum,sum1;
    scanf("%d",&t);
    while(t--){
    
    
        scanf("%d",&n);
        sum=0;
        sum1=0;
        memset(f,0,sizeof(f));
        for(i=0;i<n;i++){
    
    
            scanf("%d",&a[i].time);
        }
        for(i=0;i<n;i++){
    
    
            scanf("%d",&a[i].score);
            sum+=a[i].score;
        }
        sort(a,a+n,cmp);
        for(i=0;i<n;i++){
    
    
            for(j=a[i].time;j>0;j--)
            {
    
    
               f[j]=max(f[j],f[j-1]+a[i].score);//背包问题,求第j天能获得的最大分数
            }
        }
        for(i=1;i<=a[n-1].time;i++){
    
    
            sum1=max(sum1,f[i]);//求出最大的分数
        }
        printf("%d\n",sum-sum1);//所有的分数减去能获得的分数
    }
    return 0;
}

HOORUS 巨献

猜你喜欢

转载自blog.csdn.net/qq_52480906/article/details/114269448