基础DP入门 [ HDU 2084 ] 数塔(数字三角形)and [ HDU 1176 ] 免费馅饼 and [ HDU 1257 ] 最少拦截系统 and [ HDU 1421 ] 搬寝室

基础DP入门

  
 

HDU 2084 数塔

链接:http://acm.hdu.edu.cn/showproblem.php?pid=2084
或者AcWing(y总讲的很好):https://www.acwing.com/problem/content/900/

Problem Description

在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:

有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
在这里插入图片描述
已经告诉你了,这是个DP的题目,你能AC吗?

Input

输入数据首先包括一个整数C,表示测试实例的个数,每个测试实例的第一行是一个整数N(1 <= N <= 100),表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数,且所有的整数均在区间[0,99]内。

Output

对于每个测试实例,输出可能得到的最大和,每个实例的输出占一行。

Sample Input

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

Sample Output

30

  
 

思路

    这题直接枚举出每个路径然后比较大小显然不行,很容易想到动态规划,(数据虽然很弱,看不出什么)。
可以发现求到每个点的数字和最大都可以化作求相同的子问题加上自己。且每个子问题最优,最后的解才是最优。

而根据经验,这种样子很自然的可以将状态表达设为 f [ i ] [ j ] f[i][j] f[i][j] , 分别表示到每一个点的数字和的最大值。
对于数塔问题,我们一般有两种思路遍历求解:自上而下、自下而上。

自上而下

这样遍历求解的话,每个状态表达 f [ i ] [ j ] f[i][j] f[i][j]就代表一个集合,这个集合表示从最上方顶点到 ( i , j ) (i, j) (i,j)这个位置的所有路径。而这个集合可以再分成两部分,一部分是从左上方到达这个点,另一部分是从右上方到达这个点,(边界条件就只有一部分)再计算这个集合中两个部分数字和那个大,即接下来要分析的状态计算。

在这里插入图片描述

图来自(https://blog.csdn.net/yanweiqi1754989931/article/details/113096389#comments_14882773)感谢!

在这里插入图片描述
当经过某个节点A时,到达A路径上数字的最大和 = A左上方和右上方两个节点B、C中所能达到数字最大和中较大的那一个 + 节点A的数字,我们将其写成状态转移方程:

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − 1 ] ) + n u m [ i ] [ j ] f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − 1 ] ) + n u m [ i ] [ j ] f[i][j]=max(f[i1][j],f[i1][j1])+num[i][j]

其中, i , j i , j i,j分别表示行列的坐标, f f f 数组为维护最大和的数组,num数组储存的是输入的数字

在这个例子中,我们选取第三行的1来分析:
我们首先求左上角的数字3和右上角的数字8其能达到的最大和:
3显然只有7—3一条路径,故最大和是10;8显然也只有7—8一条路径,其最大和是15;
两者中较大的是15,故经过1所能达到的最大和是15+1=16。
循环对每个节点进行计算后,最后一行扫一遍,得到的最大值即为结果

这样我们就知道了状态计算的方法。

同理可知自下而上的方法,自下而上求解就可以少一些边界判断,当然你也可以自上而下时扩大点数组,超出边界的地方设为0就也没影响,不需要再判断,这里求写代码方便就用自下而上的方法考虑写代码。

自下而上

与自上而下型不同的是,在状态转移时,变成从该数字的左下角和右下角(对于原始数塔的位置而言)来取max了。

在这里插入图片描述

图来自(https://blog.csdn.net/yanweiqi1754989931/article/details/113096389#comments_14882773)感谢!

而这样就不会存在边界条件的不同,每个点求解都是通过下面的两个点往上。且少了最后一步求末行max的过程,最高点储存的即是最大值。

状态转移方程:

f [ i ] [ j ] = m a x ( f [ i + 1 ] [ j ] , f [ i + 1 ] [ j + 1 ] ) + n u m [ i ] [ j ] f[ i ][ j ] = max ( f[ i + 1 ][ j ] , f[ i + 1 ][ j + 1 ] ) + num [i][j] f[i][j]=max(f[i+1][j],f[i+1][j+1])+num[i][j]

代码实现

#include<iostream>
#include<algorithm>

using namespace std;

const int  N = 510;
int n;
int w[N][N],f[N][N];

int main()
{
    
    
    int c;
    cin >> c;
    while(c --)
    {
    
    
        cin >> n;
        for(int i = 1; i <= n; i ++)
            for(int j = 1 ;j <= i; j ++)
                cin >> w[i][j];
        for (int i = 1; i <= n; i ++) f[n][i]=w[n][i];
        for (int i = n - 1; i; i --)
        	for (int j = 1; j <= i; j ++)
        		f[i][j]=w[i][j]+max(f[i+1][j],f[i+1][j+1]);
        cout << f[1][1] << endl;
    }
    
    return 0;
}

  
 
 

HDU 1176 免费馅饼

链接:http://acm.hdu.edu.cn/showproblem.php?pid=1176

Problem Description

都说天上不会掉馅饼,但有一天gameboy正走在回家的小径上,忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了,这馅饼别处都不掉,就掉落在他身旁的10米范围内。馅饼如果掉在了地上当然就不能吃了,所以gameboy马上卸下身上的背包去接。但由于小径两侧都不能站人,所以他只能在小径上接。由于gameboy平时老呆在房间里玩游戏,虽然在游戏中是个身手敏捷的高手,但在现实中运动神经特别迟钝,每秒种只有在移动不超过一米的范围内接住坠落的馅饼。现在给这条小径如图标上坐标:
在这里插入图片描述
为了使问题简化,假设在接下来的一段时间里,馅饼都掉落在0-10这11个位置。开始时gameboy站在5这个位置,因此在第一秒,他只能接到4,5,6这三个位置中其中一个位置上的馅饼。问gameboy最多可能接到多少个馅饼?(假设他的背包可以容纳无穷多个馅饼)

Input

输入数据有多组。每组数据的第一行为以正整数n(0<n<100000),表示有n个馅饼掉在这条小径上。在结下来的n行中,每行有两个整数x,T(0<T<100000),表示在第T秒有一个馅饼掉在x点上。同一秒钟在同一点上可能掉下多个馅饼。n=0时输入结束。

Output

每一组输入数据对应一行输出。输出一个整数m,表示gameboy最多可能接到m个馅饼。
提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。

Sample Input

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

Sample Output

4

 
 

思路

在这里插入图片描述

图来自(https://blog.csdn.net/yanweiqi1754989931/article/details/113096389#comments_14882773)感谢!

样例的接法是:第1s移动到 x = 6 接住 x = 6 的一个馅饼,第2s移动到 x = 7,接住 x = 7 上的两个馅饼,第3s移动到 x = 8,接住 x = 8 的一个馅饼,共计4个馅饼。
看完样例,我们继续分析题目:坐标点是固定的,每个点能接到的馅饼数量也是可统计的,而画出这个图后,加上时间坐标轴,是不是有点像上面的数字三角形了,于是我们可以将问题转换为数塔模型。只不过上一题是最多两种选择,而这里有三种选择,以及这里需要多注意的边界条件。

在这里插入图片描述
如同图中的坐标轴,我们将时间维度视为数塔模型中的“行”,列不需要转义,就构成了数塔。同样求和最大。
样例模拟一下就是
第一次计算2s那一行,得到下图结果
在这里插入图片描述
第二次计算1s那一行,得到下图结果
在这里插入图片描述
最后得到结果
在这里插入图片描述

得益于自下向上分析最终结论的优越性:终点列最高行即为答案,因此我们采用自下而上型解法。而这里的下就是最后一秒往前推,容易知道状态转移方程:
d p [ i ] [ j ] = d p [ i ] [ j ] + m a x ( m a x ( d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j + 1 ] ) , d p [ i + 1 ] [ j − 1 ] ) d p [ i ] [ j ] = d p [ i ] [ j ] + m a x ( m a x ( d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j + 1 ] ) , d p [ i + 1 ] [ j − 1 ] ) dp[i][j]=dp[i][j]+max(max(dp[i+1][j],dp[i+1][j+1]),dp[i+1][j1])

 
 

代码实现

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

int dp[100009][15];

int main(){
    
    
    int n,x,t;
    while(scanf("%d", &n) != EOF && n){
    
    
        int m = 0;
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i < n; i ++){
    
    
            scanf("%d %d", &x, &t); 
            dp[t][x] ++; //每个时间点每个位置的馅饼数
            m = max(t, m);
        }
        for (int i = m - 1; i >= 0; i --){
    
     //从后往前算
            for (int j = 0; j <= 10; j ++){
    
    
            	int mx = 0;
            	if(j == 0)  mx = dp[i + 1][j + 1];
            	else if(j == 10)  mx = dp[i + 1][j - 1];
                else{
    
    
                	mx = max(dp[i + 1][j - 1], dp[i + 1][j + 1]);//两边位置下一秒的求得馅饼数的最大值
                }
                dp[i][j] += max(dp[i + 1][j], mx);
            }
        }
        printf("%d\n",dp[0][5]);
    }
    
    return 0;
}

 
 
 

HDU 1257 最少拦截系统

链接:http://acm.hdu.edu.cn/showproblem.php?pid=1257

Problem Description

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能超过前一发的高度.某天,雷达捕捉到敌国的导弹来袭.由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹.
怎么办呢?多搞几套系统呗!你说说倒蛮容易,成本呢?成本是个大问题啊.所以俺就到这里来求救了,请帮助计算一下最少需要多少套拦截系统.

Input

输入若干组数据.每组数据包括:导弹总个数(正整数),导弹依此飞来的高度(雷达给出的高度数据是不大于30000的正整数,用空格分隔)

Output

对应每组数据输出拦截所有导弹最少要配备多少套这种导弹拦截系统.

Sample Input

8 389 207 155 300 299 170 158 65

Sample Output

2

思路一

最少拦截系统其实就是一道经典的最长递增子序列的模板题,对于题目给出的数据,我们依次遍历,首先初始化第一个数为1,因为以第一个数结尾的LIS为1,我们往后遍历,每次遍历的同时,我们在当前数 n u m [ i ] num[i] num[i]前面寻找比它小的数但又是小的数里面 f [ i ] f[i] f[i]最大的数( f [ i ] f[ i ] f[i]表示以 n u m [ i ] num[i] num[i]结尾的最长递增子序列的元素个数),找到了那我们便在以当前数结尾的LIS加一,同时更新ans的值。循环结束的时候,ans就是我们要求的值。(最长递增子序列的题有时间再补在这个前面)
以样例为例:
在这里插入图片描述

最长递增子序列的元素个数为2个,所以结果就是需要2个系统。
直观上我们可以证明为什么是这个最长上升子序列的长度,可以发现在最长上升子序列中的每个元素时必然不在一个拦截系统里,所以至少需要这么多。
再来说明一下为什么本质是求最长上升子序列
根据题意,我们可以分析知道本题实质上求的是最少分为几个“最长不上升序列”!即:几条不上升“链条”
而如何求“链条数”?
这其中的数学原理是Dilworth定理,感兴趣可以看看,定理的结论是:对于一个偏序集,最少链划分数等于最长反链长度。
故可以得到这题的结论:求最长上升子序列的长度!
 
 

代码实现

#include <iostream>
#include <cstdio>
#include<cstring>
using namespace std;

int a[1005], dp[1005];
int main()
{
    
    
   
    int n, ans = 0;
    while(scanf("%d", &n) != EOF){
    
    
        ans = 0;
        for (int i = 1; i <= n; ++i)
            scanf("%d", &a[i]);
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= n; ++i) //扫描找最大上升子序列
        {
    
    
            int ma = 0; //存前面a[i]小的位置的最大的dp[j]值
            for (int j = 1; j < i; ++j)
            {
    
    
                if (a[j] < a[i])
                    ma = max(ma, dp[j]);
            }
            dp[i] = ma + 1; //个数加上自身的一个
            ans = max(ans, dp[i]);
        }
        printf("%d\n", ans);
   }
    return 0;
}

 
 

思路二

除了上面的DP做法外,很容易想到的其实贪心算法。
我们可以这样贪:
贪心流程:
从前往后扫描每一个数,对于每个数:
情况一: 如果现有的子序列的结尾都小于当前数,则创建新子序列;
情况二:将当前数放到结尾大于等于它的最小的子序列后面;(很容易理解,如果有很多个序列可以给你放,让你放在末尾,选择结尾最小的子序列影响更小,可以给后面更多的机会)
可以通过调整法来证明这样子贪心确实可以得到最优解。
 
 

代码实现

#include <sstream>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100005;

int n;
int h[N], q[N];//q数组存储现有所有子序列的结尾

int main()
{
    
    
    while(cin >> n){
    
    
        for(int i = 0; i < n; i++) cin >> h[i];
        int cnt = 0; //当前子序列个数
        for (int i = 0; i < n; i ++ ) //从前往后贪心
        {
    
    
            int k = 0; //从前往后找的序列
            while (k < cnt && q[k] < h[i]) k ++ ;
            q[k] = h[i];
            if (k == cnt) cnt ++;
        }
        printf("%d\n", cnt);
    }
    return 0;
}

 
还有另一种贪心策略
1.从左往后找第一个没有被标记的点,
2.以此点为最高点标,从左往右标记所有非严格单点递减的点
3.直到所有点都没标记
4.最后结果为最高点的个数

这个做法本质上和上面的做法类似,不同点在于这里是每次找“起点最靠前的子序列”,上面是每次找结尾最小的子序列,而可以用数学归纳法证明,起点越靠前的子序列,结尾也一定越小。
 

代码实现

#include<iostream>
#include<cstring>

using namespace std;

const int N = 1005;
int n,m;
int vis[N];
int h[N];

int main(){
    
    
    while(cin >> n){
    
    
    	memset(h, 0, sizeof(h));
    	memset(vis, 0, sizeof(vis));
	    for(int i = 1; i <= n; i++) cin >> h[i];
	    int cnt = 0;
	    for(int i = 1; i <= n; i++){
    
    
	        if(vis[i] == 1)continue;//如果该点被访问过,则continue
	        int t = h[i];//没有访问过则以该点为最高点,标记所有单调递减的导弹
	        vis[i] = 1;
	        cnt++;
	        for(int j = i + 1; j <= n; j++){
    
    
	            if(vis[j] == 1) continue;
	            if(h[j] <= t){
    
    //标记所有非严格单调递减的导弹
	                vis[j] = 1;
	                t = h[j];
	            }
	        }
	    }
	    cout << cnt << endl;
    }
    return 0;
}

HDU 1421 搬寝室

链接:http://acm.hdu.edu.cn/showproblem.php?pid=1421

Problem Description

搬寝室是很累的,xhd深有体会.时间追述2006年7月9号,那天xhd迫于无奈要从27号楼搬到3号楼,因为10号要封楼了.看着寝室里的n件物品,xhd开始发呆,因为n是一个小于2000的整数,实在是太多了,于是xhd决定随便搬2k件过去就行了.但还是会很累,因为2k也不小是一个不大于n的整数.幸运的是xhd根据多年的搬东西的经验发现每搬一次的疲劳度是和左右手的物品的重量差的平方成正比(这里补充一句,xhd每次搬两件东西,左手一件右手一件).例如xhd左手拿重量为3的物品,右手拿重量为6的物品,则他搬完这次的疲劳度为 ( 6 − 3 ) 2 = 9 (6-3)^2= 9 (63)2=9.现在可怜的xhd希望知道搬完这2*k件物品后的最佳状态是怎样的(也就是最低的疲劳度),请告诉他吧.

Input

每组输入数据有两行,第一行有两个数n,k(2<=2*k<=n<2000).第二行有n个整数分别表示n件物品的重量(重量是一个小于2^15的正整数).

Output

对应每组输入数据,输出数据只有一个表示他的最少的疲劳度,每个一行.

Sample Input

2 1
1 3

Sample Output

4

思路

对于一次操作,显然提的物品重量越接近越好,是不是可以贪心呢,显然不能贪最相近的物品来选择,这样大局满足不了,比如只有四个数:1 4 5 8,需要搬两趟,如果选择4 5和1 8显然不如1 4 和 5 8。
根据第一感觉,那么是否每次提的物品一定是重量相邻的物品呢?可以证明这样选择确实是最优的。
假设四个从小到大的数:a、b、c、d,只需证明以下表达式成立即可:
( a − b ) 2 + ( c − d ) 2 < ( a − c ) 2 + ( b − d ) 2 (a-b)^2+(c-d)^2<(a-c)^2+(b-d)^2 (ab)2+(cd)2<(ac)2+(bd)2
( a − b ) 2 + ( c − d ) 2 < ( a − d ) 2 + ( b − c ) 2 (a-b)^2+(c-d)^2<(a-d)^2+(b-c)^2 (ab)2+(cd)2<(ad)2+(bc)2
而易知上述式子成立(展开即可)
所以首先要做的工作就是先进行排序一下。

有上述结论我们继续分析:(前提都排好序了)
2个物品选一对疲劳最小,结论显然
3个物品选一对疲劳最小,考虑两个相邻的,比较得答案
4个物品选一对疲劳最小,这就比较普适了,可以分成两种情况,要么包含第四个物品,要么不包含第四个(即只需看其子问题,看前三个)。
这样子就具有一般性了。
可以推广到n个物品选一度对,也是分为要么包含第n个(即选第n-1个和第n个),要么不包含第n个。

接下来我们看n个物品选两对(即搬两趟)
可以知道4个物品搬两趟,就是选2个相邻的
再看5个物品搬两趟,就是比较包含第五个物品(即选第四个和第五个算一趟)和不包含第五个物品(即前四个搬两趟的最优解)的情况进行比较。
以此推广
到n个物品搬两趟,分为包含第n个(即选第n-1个和第n个)和不包含第n个

以此类推,到n个物品选k对( n ≥ k n≥k nk

我们考虑用 d p [ n ] [ k ] dp[n][k] dp[n][k] 表示从前n个物品中选出 2 ∗ k 2*k 2k个物品的最小疲劳度来表达每个状态。
故求解每个 d p [ i ] [ j ] dp[i][j] dp[i][j]的值时
对第 i i i 件物品,若不选第 i i i 件物品
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j] , 前提是 2 ∗ j ≤ i − 1 2*j ≤ i -1 2ji1
否则若选则第 i i i 件物品,则与它的差最小的则是第i-1件物品,那么第 i − 1 件 i-1件 i1物品也应被选择
d p [ i ] [ j ] = d p [ i − 2 ] [ j − 1 ] + ( a [ i ] − a [ i − 1 ] ) ∗ ( a [ i ] − a [ i − 1 ] ) dp[i][j] = dp[i-2][j-1] + (a[i]-a[i-1])*(a[i]-a[i-1]) dp[i][j]=dp[i2][j1]+(a[i]a[i1])(a[i]a[i1])
其中 a [ i ] a[i] a[i] 表示重量
只要取二者的min()就ok了,得到状态转移方程是: d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i − 2 ] [ j − 1 ] + ( a [ i ] − a [ i − 1 ] ) ∗ ( a [ i ] − a [ i − 1 ] ) ) dp[i][j] = min(dp[i-1][j], dp[i-2][j-1] + (a[i] - a[i-1]) * (a[i] - a[i-1])) dp[i][j]=min(dp[i1][j],dp[i2][j1]+(a[i]a[i1])(a[i]a[i1]))

代码实现

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
using namespace std;

int a[2010],dp[2010][1010]; //dp[i][j]表示有i个物品,搬j趟
int main(){
    
    
    int n, k;
    while(scanf("%d%d", &n, &k) != EOF){
    
        
        for(int i = 1; i <= n; i ++)
            scanf("%d", &a[i]);
        sort(a + 1, a + n + 1); //先按升序排列
        for(int i = 1; i <= k; i ++)
            for(int j = 2 * i; j <= n; j ++){
    
    
                dp[j][i] = dp[j - 2][i - 1] + (a[j] - a[j - 1])*(a[j] - a[j - 1]); //选第j件,所相邻的也选
                //这里加不加if其实无所谓(指if语句,后面求min还是要的),加了可以少算一点,因为j==i*2只有一种方案,全部都得搬。
                if(j != i * 2) dp[j][i]=min(dp[j][i], dp[j - 1][i]); //选第j件和不选第j件两种结果中取最小   
            }
        printf("%d\n", dp[n][k]);        
    }
    return 0;
}

这里的dp数组其实也可以只开 d p [ 3 ] [ 1005 ] dp[3][1005] dp[3][1005],因为dp[i][j]最多只会用到dp[i-1][]和dp[i-2][],对于dp[i-3][]和之前的数据是没有引用的,也就是说这些数据都是无效的了所以可以只开三个然后运用滚动数组的技巧,来循环利用数组。
再改下状态计算的表达式即可:
d p [ i % 3 ] [ j ] = m i n ( d p [ ( i − 1 ) % 3 ] [ j ] , d p [ ( i − 2 ) % 3 ] [ j − 1 ] + ( a [ i ] − a [ i − 1 ] ) ∗ ( a [ i ] − a [ i − 1 ] ) ) dp[i\%3][j] = min(dp[(i-1)\%3][j], dp[(i-2)\%3][j-1] + (a[i] - a[i-1]) * (a[i] - a[i-1])) dp[i%3][j]=min(dp[(i1)%3][j],dp[(i2)%3][j1]+(a[i]a[i1])(a[i]a[i1]))
 
 

代码实现

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
int a[2010],dp[3][1010]; 
int main(){
    
    
    int n,k;
    while(scanf("%d%d", &n, &k)!=EOF){
    
        
        for(int i = 1; i <= n; i ++)
            scanf("%d", &a[i]);
        for (int i = 0; i <= 2; ++i){
    
    
            for (int j = 1; j < k + 1; ++j){
    
    
                dp[i][j] = INF;
            }
            dp[i][0] = 0;
        }
        sort(a + 1,a + n + 1); //先按升序排列
        for(int j = 2; j <= n; j ++){
    
    
            for(int i = 1; i <= k; i ++){
    
    
                dp[j % 3][i]=min(dp[(j - 1) % 3][i], (dp[(j - 2) % 3][i - 1] + (a[j] - a[j - 1]) * (a[j] - a[j - 1]))); //选第j件和不选第j件两种结果中取最小   
            }
        }
        printf("%d\n",dp[n%3][k]);        
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/xxmy7/article/details/113574013