ヾ(o◕∀◕)ノヾ各种动态规划经典例题(新手向、多类型)

ヾ(o◕∀◕)ノヾ各种动态规划经典例题(新手向、多类型)

一、前言

ヾ(・ω・`。)我把比较常见的类型的动态规划找了一些经典的例题,适合作为新手的入门例题,用于帮助我们对各种不同的动态规划有所了解,很多题也可以当做模板。内容比较多,可以先一键三连然后慢慢看(ಡωಡ)hiaia因为笔者也是萌新蒟蒻,所以如果哪里解释的不清楚或者大佬看出来哪里有问题的话请多多评论,蟹蟹٩(‘ω’)و

二、线性dp

线性dp往往比较容易就能找到状态转移方程,逻辑上也比较清楚,感觉和递推有点像。我们来看看题

1、守望者逃离

题目描述

恶魔猎手尤迪安野心勃勃,他背叛了暗夜精灵,率领深藏在海底的娜迦族企图叛变。守望者在与尤迪安的交锋中遭遇了围杀,被困在一个荒芜的大岛上。为了杀死守望者,尤迪安开始对这个荒岛施咒,这座岛很快就会沉下去。到那时,岛上的所有人都会遇难。守望者的跑步速度为17m/s,以这样的速度是无法逃离荒岛的。庆幸的是守望者拥有闪烁法术,可在1s内移动60m,不过每次使用闪烁法术都会消耗魔法值10点。守望者的魔法值恢复的速度为4点/s,只有处在原地休息状态时才能恢复。

现在已知守望者的魔法初值M,他所在的初始位置与岛的出口之间的距离S,岛沉没的时间T。你的任务是写一个程序帮助守望者计算如何在最短的时间内逃离荒岛,若不能逃出,则输出守望者在剩下的时间内能走的最远距离。注意:守望者跑步、闪烁或休息活动均以秒(s)为单位,且每次活动的持续时间为整数秒。距离的单位为米(m)。

输入格式

共一行,包括空格隔开的三个非负整数M,S,T

输出格式

共两行。

第11行为字符串“Yes”或“No”(区分大小写),即守望者是否能逃离荒岛。

第22行包含一个整数。第一行为“Yes”(区分大小写)时表示守望者逃离荒岛的最短时间;第一行为“No*”(区分大小写)时表示守望者能走的最远距离。

输入输出样例
输入 #1
39 200 4
输出 #1
No
197
输入 #2
36 255 10
输出 #2
Yes
6
说明/提示

30%的数据满足:1≤T≤10,1≤S≤100

50%的数据满足:1≤T<≤1000,1≤S≤10000

100%的数据满足:1≤T≤300000,0≤M≤1000,1≤S≤10^8.

这道题其实如果我们不用动态规划的话,通过模拟也一定能写出来的。每一秒我们无非3种选择,休息,魔法和跑,显然当我们有魔法的时候一定使用魔法,所以我们只需要考虑休息和跑的关系,动态规划的关系也很好找,这一秒的跑的距离之和上一秒以及这一秒的选择有关系,所以我们假设不走,全都休息和魔法来设定初始化,然后在动态规划每一秒跑与不跑的情况。

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int dp[10000009];
int main()
{
    ios::sync_with_stdio(false);
    int M,S,T;
    cin >> M >> S >>T;
    dp[0] = 0;
    for(int i = 1 ; i <= T; i++){
        if(M >= 10){
            M -= 10;
            dp[i] = dp[i-1] + 60;
        }
        else{
            M += 4;
            dp[i] = dp[i-1];
        }
    }
    for(int i = 1; i <= T; i++){
        if(dp[i] < dp[i - 1] + 17){
            dp[i] = dp[i - 1] + 17;
        }
        if(dp[i] >=  S){
            cout<<"Yes"<<endl;
            cout<<i<<endl;
        return 0;
        }
    }
    cout<<"No"<<endl;
    cout<<dp[T]<<endl;
    return 0;
}

2、摆花

题目描述

小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共m盆。通过调查顾客的喜好,小明列出了顾客最喜欢的n种花,从1到n标号。为了在门口展出更多种花,规定第i种花不能超过ai盆,摆花时同一种花放在一起,且不同种类的花需按标号的从小到大的顺序依次摆列。

试编程计算,一共有多少种不同的摆花方案。

输入格式

第一行包含两个正整数nm,中间用一个空格隔开。

第二行有n个整数,每两个整数之间用一个空格隔开,依次表示a1,a2,…,an

输出格式

一个整数,表示有多少种方案。注意:因为方案数可能很多,请输出方案数对1000007取模的结果。

输入输出样例
输入 #1
2 4
3 2
输出 #1
2
说明/提示

【数据范围】

对于20%数据,有0<n≤8,0<m≤8,0≤ai≤8;

对于50%数据,有0<n≤20,0<m≤20,0≤ai≤20;

对于100%数据,有0<n≤100,0<m≤100,0≤ai≤100。

NOIP 2012 普及组 第三题

这道题我们要一种一种花来动态规划,我们使用n种花m盆的情况数为我们使用n-1种花,不到m盆所有情况的和加1,状态转移方程还是看代码,因为这里主要是讲线性dp的思想,没有对代码规划太多~~(一星期时间太紧┭┮﹏┭┮)~~

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int a[1009];
int dp[1009][1009];
int main()
{
    ios::sync_with_stdio(false);
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        cin >> a[i];
    }
    dp[0][0] = 1;
    for(int i = 1; i <= n; i++){
        for(int j = 0; j <= m; j++){
            for(int k = 0; k <= a[i] && k <= j; k++){
                dp[i][j] = ( dp[i][j] + dp[i-1][j-k] ) % 1000007;
            }
        }
    }
    cout << dp[n][m];
    return 0;
}

3、线段

题目描述

在一个n×n 的平面上,在每一行中有一条线段,第 ii 行的线段的左端点是(i,Li),右端点是(i,Ri)。

你从 (1,1) 点出发,要求沿途走过所有的线段,最终到达 (n,n) 点,且所走的路程长度要尽量短。

更具体一些说,你在任何时候只能选择向下走一步(行数增加 1)、向左走一步(列数减少 1)或是向右走一步(列数增加 1)。当然,由于你不能向上行走,因此在从任何一行向下走到另一行的时候,你必须保证已经走完本行的那条线段。

输入格式

第一行有一个整数 n

以下 n 行,在第 i 行(总第 (i+1) 行)的两个整数表示 LiRi

输出格式

仅包含一个整数,你选择的最短路程的长度。

输入输出样例
输入 #1
6
2 6
3 4
1 3
1 2
3 6
4 5
输出 #1
24
说明/提示

我们选择的路线是

 (1, 1) (1, 6)
 (2, 6) (2, 3)
 (3, 3) (3, 1)
 (4, 1) (4, 2)
 (5, 2) (5, 6)
 (6, 6) (6, 4) (6, 6)

不难计算得到,路程的总长度是 24。

对于 100% 的数据中,n≤2×10^4,1≤LiRin

这道题我们走完某一行时的路程总数取决于上一行走完的路程总数加上这一行的走法,从上一行下来的点无非是上一行线段的左端点或右端点,而这一行的走法也无非是先走左端点后走右端点或先走右端点再走左端点,比一比哪个小就是了,思路还是比较清楚的

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int res[20009][2];
int a[20009][2];
int dis[20009];
int main()
{
    ios::sync_with_stdio(false);
    int n = r;
    for(int i = 1; i <= n; i++){
        a[i][0] = r;
        a[i][1] = r;
        dis[i] = abs(a[i][0] - a[i][1]);
    }
    res[1][0] = (dis[1]) * 2 + a[1][0] - 1;
    res[1][1] = dis[1] + a[1][0] - 1;
    for(int i = 2; i <= n; i++){
        res[i][0] = min(res[i-1][0] + 1 + dis[i] + abs(a[i-1][0] - a[i][1]),res[i-1][1] + 1 + dis[i] + abs(a[i-1][1] - a[i][1]));
        res[i][1] = min(res[i-1][0] + 1 + dis[i] + abs(a[i-1][0] - a[i][0]),res[i-1][1] + 1 + dis[i] + abs(a[i-1][1] - a[i][0]));
    }
    cout<< min(res[n][0] + abs(a[n][0] - n), res[n][1] + abs(a[n][1] - n))<<endl;
    return 0;
}

三、背包问题

背包问题我觉得是动态规划里面最形象的一类问题,他的思路就是判断每个物品拿或不拿,然后取最值。当然具体分的话也是很复杂的,这里主要带来一些经典的01背包,完全背包,多重背包和一些其他有意思的题,比较全面的背包问题有机会再单独来说ค(TㅅT)

1、采药

题目描述

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

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

输入格式

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

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

输出格式

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

输入输出样例
输入 #1
70 3
71 100
69 1
1 2
输出 #1
3
说明/提示
【数据范围】
  • 对于 30% 的数据,M≤10;
  • 对于全部的数据,M≤100。
【题目来源】

NOIP 2005 普及组第三题

这是最普通的背包问题的模板,01背包,我们只需要讨论每个物品是否要放入背包即可

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int dp[1005];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int main()
{
    ios::sync_with_stdio(false);
    int T = r;
    int M = r;
    for(int i = 1; i <= M; i++){
        int t = r;
        int v = r;
        for(int j = T; j >= t; j--){
            dp[j] = max(dp[j],dp[j-t]+v);
        }
    }
    cout<<dp[T]<<endl;
    return 0;
}

2、疯狂的采药

题目背景

此题为纪念 LiYuxiang 而生。

题目描述

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

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

此题和原题的不同点:

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

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

输入格式

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

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

输出格式

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

输入输出样例
输入 #1
70 3
71 100
69 1
1 2
输出 #1
140
说明/提示
数据规模与约定
  • 对于 30% 的数据,保证 m≤10^3 。
  • 对于 100% 的数据,保证 1≤m≤10^4, 1≤t≤10^7 ,且 1≤m×t≤10^7, 1≤ai,*b**≤10^4。

这是多重背包的模板,和01背包不同的是每个物品都可以无限取,所以主要是遍历的方式不一样,要从前往后遍历,这样当遍历后面的值的时候,会考虑到前面已经取过该物品的情况(这也是前面01背包为什么从后向前遍历)

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
long long dp[10000005];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int main()
{
    ios::sync_with_stdio(false);
    int T = r;
    int M = r;
    for(int i = 1; i <= M; i++){
        int t = r;
        int v = r;
        for(int j = t; j <= T; j++){
            dp[j] = max(dp[j],dp[j-t]+v);
        }
    }
    cout<<dp[T]<<endl;
    return 0;
}

3、[NOIP2014 提高组] 飞扬的小鸟

题目描述

Flappy Bird 是一款风靡一时的休闲手机游戏。玩家需要不断控制点击手机屏幕的频率来调节小鸟的飞行高度,让小鸟顺利通过画面右方的管道缝隙。如果小鸟一不小心撞到了水管或者掉在地上的话,便宣告失败。

为了简化问题,我们对游戏规则进行了简化和改编:

游戏界面是一个长为 n,高为 m* 的二维平面,其中有 k* 个管道(忽略管道的宽度)。

小鸟始终在游戏界面内移动。小鸟从游戏界面最左边任意整数高度位置出发,到达游戏界面最右边时,游戏完成。

小鸟每个单位时间沿横坐标方向右移的距离为 1,竖直移动的距离由玩家控制。如果点击屏幕,小鸟就会上升一定高度 x,每个单位时间可以点击多次,效果叠加;如果不点击屏幕,小鸟就会下降一定高度 y。小鸟位于横坐标方向不同位置时,上升的高度 x 和下降的高度 y 可能互不相同。

小鸟高度等于 0 或者小鸟碰到管道时,游戏失败。小鸟高度为 m 时,无法再上升。

现在,请你判断是否可以完成游戏。如果可以,输出最少点击屏幕数;否则,输出小鸟最多可以通过多少个管道缝隙。

输入格式

第 1 行有 3 个整数 n,m,k,分别表示游戏界面的长度,高度和水管的数量,每两个整数之间用一个空格隔开;

接下来的 n 行,每行 2 个用一个空格隔开的整数 xy,依次表示在横坐标位置 0∼n−1 上玩家点击屏幕后,小鸟在下一位置上升的高度 x,以及在这个位置上玩家不点击屏幕时,小鸟在下一位置下降的高度 y

接下来 k 行,每行 3 个整数 p,l,h,每两个整数之间用一个空格隔开。每行表示一个管道,其中 p 表示管道的横坐标,l 表示此管道缝隙的下边沿高度,h 表示管道缝隙上边沿的高度(输入数据保证 p 各不相同,但不保证按照大小顺序给出)。

输出格式

共两行。

第一行,包含一个整数,如果可以成功完成游戏,则输出 1,否则输出 0。

第二行,包含一个整数,如果第一行为 1,则输出成功完成游戏需要最少点击屏幕数,否则,输出小鸟最多可以通过多少个管道缝隙。

输入输出样例
输入 #1
10 10 6 
3 9  
9 9  
1 2  
1 3  
1 2  
1 1  
2 1  
2 1  
1 6  
2 2  
1 2 7 
5 1 5 
6 3 5 
7 5 8 
8 7 9 
9 1 3 
输出 #1
1
6
输入 #2
10 10 4 
1 2  
3 1  
2 2  
1 8  
1 8  
3 2  
2 1  
2 1  
2 2  
1 2  
1 0 2 
6 7 9 
9 1 4 
3 8 10  
输出 #2
0
3
说明/提示

【输入输出样例说明】

如下图所示,蓝色直线表示小鸟的飞行轨迹,红色直线表示管道。

img

【数据范围】

对于 30% 的数据:5≤n≤10,5≤m≤10,k=0,保证存在一组最优解使得同一单位时间最多点击屏幕 3 次;

对于 50% 的数据:5≤n≤20,5≤m≤10,保证存在一组最优解使得同一单位时间最多点击屏幕 3 次;

对于 70% 的数据:5≤n≤1000,5≤m≤100;

对于 100% 的数据:5≤m≤1000,0≤k<n,0<x,y<m,0<p<n,0≤l<hml+1<h

这道题和之前的相比难度要大一些了,上升的过程可以看做是多重背包(可以不断取),下降的过程可以看做是01背包(只能一次)除此之外,我们还需要去掉管道的位置。还要判断如果不能达到终点的话能过几个水管,主要是比较复杂,需要细心,思考上难度也不是很大

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int dp[10008][2008];
int x[10008];
int y[10008];
int l[10008];
int h[10008];
bool con[10008];
int main()
{
    ios::sync_with_stdio(false);
    int n = r;
    int m = r;
    int k = r;
    for(int i = 1; i <= n; i++){
        x[i] = r;
        y[i] = r;
    }
    for(int i = 0; i <= n; i++){
        for(int j = 0; j <= m; j++){
            dp[i][j] = 9999999;
        }
    }
    for(int i = 0 ; i <= n; i++){
        l[i] = 0;
        h[i] = m + 1;
    }
    for(int i = 1; i <= k; i++){
        int w = r;
        con[w] = 1;
        l[w] = r;
        h[w] = r;
    }
    for(int i = 1; i <= m; i++){
        dp[0][i] = 0;
    }

    for(int i = 1 ; i <= n; i++){
        //1、上升的多重背包
        for(int j = x[i]+1; j <= m + x[i] ; j++){
            dp[i][j] = min(dp[i-1][j-x[i]]+1,dp[i][j-x[i]]+1);
        }
        //2、高于m的归于m
        for(int j = m+1; j <= m + x[i]; j++){
            dp[i][m] = min(dp[i][m],dp[i][j]);
        }
        //3、下降的01背包
        for(int j = 0; j <= m - y[i]; j++){
            dp[i][j] = min(dp[i-1][j+y[i]],dp[i][j]);
        }
        //4、对水管的下边界判断
        for(int j = 0; j <= l[i]; j++){
            dp[i][j] = dp[0][0];
        }
        //5、对水管上边界判断
        for(int j = m; j >= h[i]; j--){
            dp[i][j] = dp[0][0];
        }

    }
    int ans = dp[0][0];
    for(int i = 1; i <= m; i++){
        if(dp[n][i] < ans){
            ans = dp[n][i];
        }
    }
    if(ans == dp[0][0]){
        int temp = n;
        for(int i = n; i >= 1; i--){
            for(int j = 1; j <= m; j++){
                if(dp[i][j] < dp[0][0]){
                    temp = i;
                    break;
                }
            }
            if(temp < n){
                break;
            }
        }
        ans = 0;
        for(int i = 1; i <= temp; i++){
            if(con[i] == 1){
                ans++;
            }
        }
        cout<<"0"<<endl;
        cout<<ans<<endl;
    }
    else{
        cout<<"1"<<endl;
        cout<<ans<<endl;
    }
    return 0;
}

4、[NOIP2006 提高组] 金明的预算方案

题目描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 nn 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件附件
电脑打印机,扫描仪
书柜图书
书桌台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 个、1 个或 2 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 n 元。于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1∼5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 n 元的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 j 件物品的价格为 vj,重要度为wj,共选中了 kk 件物品,编号依j1,j2,…,jk,则所求的总和为:

vjwj1+vjwj2+⋯+vjk×wjk

请你帮助金明设计一个满足要求的购物单。

输入格式

第一行有两个整数,分别表示总钱数 n 和希望购买的物品个数 m

第 2 到第 (m+1) 行,每行三个整数,第 (i+1) 行的整数 vipiqi 分别表示第 i 件物品的价格、重要度以及它对应的的主件。如果 qi=0,表示该物品本身是主件。

输出格式

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

输入输出样例
输入 #1
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
输出 #1
2200
说明/提示
数据规模与约定

对于全部的测试点,保证 1≤n≤3.2×104,1≤m≤60,0≤vi≤10^4,1≤pi≤5,0≤qim,答案不超过 2×10^5。

这道题是带有附件的背包问题,我们要先考虑放不放主件,如果放主件,看钱够不够买附件,够的话就再看放不放附件,基本做法还是和01背包一样,就是注意数据的处理

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int find(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = find(f[k]);
}
int dp[40000];
int w[70][3];
int v[70][3];
int main()
{
    ios::sync_with_stdio(false);
    int n = r;
    int m = r;
    for(int i = 1; i <= m; i++){
        int a = r;
        int b = r;
        int c = r;
        if(c == 0){
            w[i][0] = a;
            v[i][0] = b * a;
        }
        else{
            if(w[c][1] == 0){
                w[c][1] = a;
                v[c][1] = a * b;
            }
            else{
                w[c][2] = a;
                v[c][2] = a * b;
            }
        }
    }
    //01背包
    for(int i = 1; i <= m; i++){
        for(int j = n; j >= w[i][0]; j--){
            dp[j] = max(dp[j],dp[j-w[i][0]] + v[i][0]);
            //如果能买得起附件就试试买不买附件
            if(j >= w[i][0] + w[i][1]){
                dp[j] = max(dp[j],dp[j - w[i][0] - w[i][1]] + v[i][0] + v[i][1]);
            }
            if(j >= w[i][0] + w[i][2]){
                dp[j] = max(dp[j],dp[j - w[i][0] - w[i][2]] + v[i][0] + v[i][2]);
            }
            if(j >= w[i][0] + w[i][1] + w[i][2]){
                dp[j] = max(dp[j],dp[j - w[i][0] - w[i][1] - w[i][2]] + v[i][0] + v[i][1] + v[i][2]);
            }
        }
    }
    cout<<dp[n]<<endl;
    return 0;
}

四、区间dp

区间dp比较常见的模型是给一个长链或者圆环,让按照一定规则合并,然后有不同的计分方式,然后计算整个区间的最大值或最小值。基本思想是整个大区间的最值就是每个小区间取到最值之后合并起来,然后就动态规划从最小的区间开始,计算怎么合并能取到最值即可。如果是圆环的话,还会用到断环成链,因为感觉模型差不多就没有做太多的题,这里找了一道比较经典的,后面有机会再写一些补充一下

1、石子合并

题目描述

在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。

输入格式

数据的第 1 行是正整数 N,表示有 N 堆石子。

第 2 行有 N 个整数,第 i 个整数 ai 表示第 i 堆石子的个数。

输出格式

输出共 2 行,第 1 行为最小得分,第 2 行为最大得分。

输入输出样例
输入 #1
4
4 5 9 4
输出 #1
43
54
说明/提示

1≤N≤100,0≤ai≤20。

这道题首先要对数据进行断环成链处理,就是把长度为n的环写两遍成为长度为2n的链,在这条链上能找到环当中任意的位置关系,也不难理解。这道题的思路是一个大区间的值 = 两个小区间的值 + 他们合并的值,所以只要我们能表示出他们合并的值就可以用动态规划了ヾ(◍°∇°◍)ノ゙,而且只要我们知道大区间的左右结点,就可以算出他们合并的值,当然这里可以用前缀和优化一下。然后我们动态规划,最后遍历以某个点作为开头的dp最值,求出最值中的最值

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
int a[300];
int n[300];
int dp1[300][300];
int dp2[300][300];
int main()
{
    ios::sync_with_stdio(false);
    int N  = r;
    for(int i = 1; i <= N; i++){
        a[i] = r;
        a[N + i] = a[i];
    }
    n[1] = a[1];
    for(int i = 2; i <= 2 * N; i++){
        n[i] = n[i-1] + a[i];
    }
    for(int i = 1; i <= 2 * N; i++){
        for(int j = 1; j <= 2 * N; j++){
            dp1[i][j] = 99999999;
        }
    }
    for(int i = 1; i <= 2 * N; i++){
        dp1[i][i] = 0;
    }
    //对不同长度处理
    for(int i = 2; i <= N; i++){
        //这里控制左结点,注意范围
        for(int l = 1; l <= 2 * N - i; l++){
            //长度已知,所以这里可以定下右结点
            int ri = l + i - 1;
            for(int k = l; k <= ri - 1; k++){
                dp1[l][ri] = min(dp1[l][ri],dp1[l][k] + dp1[k+1][ri] + n[ri] - n[l-1]);
                dp2[l][ri] = max(dp2[l][ri],dp2[l][k] + dp2[k+1][ri] + n[ri] - n[l-1]);
            }
        }
    }
    int minn = 99999999;
    int maxn = 0;
    for(int i = 1; i <= N; i++){
        int l = i;
        int ri = i + N - 1;
        minn = min(minn,dp1[l][ri]);
        maxn = max(maxn,dp2[l][ri]);
    }
    cout<<minn<<endl;
    cout<<maxn<<endl;
    return 0;
}

五、状压dp

状态压缩的数据量往往比较小,一行的数据可以压缩成1个数字,利用数字的二进制来表示每一位的情况,再结合位运算,能有一些意想不到的效果,但对位运算的掌握有一定的要求。

1、[NOI2001] 炮兵阵地

题目描述

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。

一个 N×M 的地图由 NM 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。

在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

img

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。

图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。

现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入格式

第一行包含两个由空格分割开的正整数,分别表示 NM

接下来的 N 行,每一行含有连续的 M 个字符,按顺序表示地图中每一行的数据。

输出格式

一行一个整数,表示最多能摆放的炮兵部队的数量。

输入输出样例
输入 #1
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
输出 #1
6
说明/提示

对于 100% 的数据,N≤100,M≤10,保证字符仅包含 ph

┭┮﹏┭┮这道题写好久我状压太弱了这道题主要是比较复杂,我们要把思路理清楚,我在代码里加了比较详细的注释,所以这里主要将一下思路,具体怎么实现大家看代码咯。首先要把数据存在一个地图当中,然后利用状态压缩把地图压缩成一个一位数组方便使用(每次只需要一个位运算就能检查一行的情况)然后先筛选出假设不考虑地图,每一行我们能考虑的各种情况,这显然是有共性的,我们把他们存起来之后考虑每一行情况就可以直接用了。然后我们知道动态规划要有初值,这次的初值就是前两行的值,所以我们要先把前两行给模拟出来,然后利用动态规划判断在本行的前两行确定的情况下,这个方案是不是最优方案,最后比较最后一行的前两行的所有情况(因为前两行一旦确定,最后一行一定是最优方案,之前有动态规划),找出最大的情况即可。还有一个很关键的事情,要注意位运算的优先级~~(当然我是在无脑加括号(゚∀゚))~~

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
int mp[105][15];//存储地图,1为高地
int F[105]; //存储状态压缩之后的地图
int p[100]; //存储在行内而言可行的情况(不考虑地形)
int q[100]; //存储在行内可行的情况的贡献值(即炮兵的数量)
int cnt = 0;//对行内情况总数进行统计。
int dp[105][100][100];//第一个数据代表行数,第二个数据代表本行的某种情况,第三个数据代表上行的某种情况
int main()
{
    ios::sync_with_stdio(false);
    int n, m;
    n = r;
    m = r;
    //接收地图
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            char c;
            scanf(" %c",&c);
            if(c == 'H'){
                mp[i][j] = 1;
            }
        }
    }
    //对地图状态压缩
    for(int i = 1 ; i <= n; i++){
        for(int j = 1; j <= m; j++){
            F[i] = (F[i] << 1) + mp[i][j];
        }
    }
    //对行内可以存在的情况进行列举
    cnt++;//存在的第一种情况是什么都不放,所以直接开始找第二种
    for(int i = 1 ; i < (1 << m); i++){
        if(i&(i<<1)){
            continue;
        }
		if(i&(i<<2)){
            continue;
		}
		if(i&(i>>1)){
            continue;
		}
		if(i&(i>>2)){
            continue;
		}
            cnt++;
            p[cnt] = i;
            int temp = i;
            for(;temp;){
                temp -= (temp & ( -1 * temp ));//位运算,去掉最末尾的1
                q[cnt]++;
            }
    }
    //先分析一下第一行的情况
    for(int i = 1; i <= cnt; i++){
        //如果和地图不冲突
        if((p[i] & F[1]) == 0){
            dp[1][i][0] = q[i];
        }
    }
    //再分析第二行,要确保第二行与第一行不冲突
    for(int i = 1; i <= cnt ; i ++ ){
        if((p[i] & F[2]) == 0){
            for(int j = 1; j <= cnt; j++){
                if(((p[i] & p[j]) == 0) && ((p[j] & F[1]) == 0)){
                    dp[2][i][j] = q[i] + q[j];
                }
            }
        }
    }
    //然后按照这个逻辑,我们就可以实现对每一行的模拟
    for(int i = 3; i <= n; i++){
        for(int j = 1; j <= cnt; j++){
            //如果方案i与地形不冲突
            if((p[j] & F[i]) == 0){
                //挑出上一行中和这个方案不冲突的
                for(int x = 1; x <= cnt; x++){
                    if(((p[x] & F[i-1]) == 0) && ((p[x] & p[j]) == 0)){
                        //再看看上上一行有哪些方案仍不冲突
                        for(int y = 1; y <= cnt; y++){
                            if((p[j] & p[y]) == 0 && (p[x] & p[y]) == 0 && (p[y] & F[i-2]) == 0){
                                //然后动态规划决定是否选择这个方案
								dp[i][j][x]=max(dp[i][j][x],dp[i-1][x][y]+q[j]);
							}
                        }
                    }
                }
            }
        }
    }
    int ans = 0;
    for(int i = 1 ; i <= cnt; i++){
        for(int j = 1; j <= cnt; j++){
            ans = max(ans, dp[n][i][j]);
        }
    }
    printf("%d",ans);
    return 0;
}

2、P3052 [USACO12MAR]Cows in a Skyscraper G

题目描述

A little known fact about Bessie and friends is that they love stair climbing races. A better known fact is that cows really don’t like going down stairs. So after the cows finish racing to the top of their favorite skyscraper, they had a problem. Refusing to climb back down using the stairs, the cows are forced to use the elevator in order to get back to the ground floor.

The elevator has a maximum weight capacity of W (1 <= W <= 100,000,000) pounds and cow i weighs C_i (1 <= C_i <= W) pounds. Please help Bessie figure out how to get all the N (1 <= N <= 18) of the cows to the ground floor using the least number of elevator rides. The sum of the weights of the cows on each elevator ride must be no larger than W.

给出n个物品,体积为w[i],现把其分成若干组,要求每组总体积<=W,问最小分组。(n<=18)

输入格式

* Line 1: N and W separated by a space.

* Lines 2…1+N: Line i+1 contains the integer C_i, giving the weight of one of the cows.

输出格式

* A single integer, R, indicating the minimum number of elevator rides needed.

one of the R trips down the elevator.

输入输出样例
输入 #1
4 10 
5 
6 
3 
7 
输出 #1
3 
说明/提示

There are four cows weighing 5, 6, 3, and 7 pounds. The elevator has a maximum weight capacity of 10 pounds.

We can put the cow weighing 3 on the same elevator as any other cow but the other three cows are too heavy to be combined. For the solution above, elevator ride 1 involves cow #1 and #3, elevator ride 2 involves cow #2, and elevator ride 3 involves cow #4. Several other solutions are possible for this input.

这道题比上一个要简单一些,同理,我们模拟每只奶牛是否在电梯上的各种情况,代表注释这次写得比较全,所以我们直接看代码

AC code
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
int dp[1 << 18];// 存储i只奶牛要乘坐电梯的次数
int weight[1 << 18];//存储放入i只奶牛后最后一个电梯的剩余重量
int a[20];//存储每头奶牛的重量
int main()
{
    ios::sync_with_stdio(false);
    int n,w;
    n = r;
    w = r;
    //初始化
    weight[0] = w;
    for(int i = 1; i <= n; i++){
        a[i] = r;
    }
    for(int i = 1; i < (1 << n); i++){
        dp[i] = 100;
    }
    dp[0] = 1;
    //开始dp,首先遍历各种情况
    for(int i = 0; i < (1 << n); i++){
        //遍历每只奶牛
        for(int j = 1; j <= n; j++){
            //如果第j只奶牛再这种情况下已经被放入电梯,那就不管了。
            if(i & (1 << (j - 1))){
                continue;
            }
            //如果能放下并且放下是节省次数的,那肯定要放下
            if(weight[i]>=a[j] && dp[i | (1<<(j-1))] >= dp[i]){
                dp[i | (1 << (j - 1))] = dp[i];
                weight[i | (1 << (j - 1))] = max(weight[i] - a[j],weight[i | (1 << (j - 1))]);
            }
            //如果放不下,但是再开一次仍然比之前要节省次数,那我们就这么做
            else if(weight[i]<a[j] && dp[i | (1<<(j-1))] >= dp[i]+1){
                dp[i | (1 << (j - 1))] = dp[i] + 1;
                weight[i | (1 << (j - 1))] = max(weight[i | (1 << (j - 1))],w - a[j]);
            }
        }
    }
    cout << dp[(1 << n) - 1];
    return 0;
}

六、数位dp

数位dp顾名思义就是对数字的每一位进行dp,其实是一种搜索,找出每位数之间符合某种条件的数。

1、windy数

题目背景

windy 定义了一种 windy 数。

题目描述

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 ab 之间,包括 ab ,总共有多少个 windy 数?

输入格式

输入只有一行两个整数,分别表示 ab

输出格式

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

输入输出样例
输入 #1
1 10
输出 #1
9
输入 #2
25 50
输出 #2
20
说明/提示
数据规模与约定

对于全部的测试点,保证 1≤ab≤2×10^9。

这道题难度不大,我们模拟出所有的windy数然后对前n个求和,再对前m+1个求和,然后求差即可。

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
int dp[15][11];//存储长度为i,最高位为j的windy数个数
//存储开头和结尾两个数的每位数
int n1[15];
int n2[15];
int main()
{
    ios::sync_with_stdio(false);
    //先遍历出所有的windy数,后面直接取用
    //初始化-------一位数的都满足条件
    for(int i = 0; i <= 9; i++){
        dp[1][i] = 1;
    }
    //dp
    for(int i = 2; i <= 15; i++){
        for(int j = 0; j < 10; j++){
            for(int k = 0; k < 10; k++){
                if(abs(j - k) >= 2){
                    dp[i][j] += dp[i-1][k];
                }
            }
        }
    }
    int l = r;
    int ri = r;
    ri++;//方便把右边界也包括进去
    int res1 = 0;
    int res2 = 0;
    int len1 = 0;
    int len2 = 0;
    while(l){
        len1++;
        n1[len1] = l % 10;
        l /= 10;

    }
    while(ri){
        len2++;
        n2[len2] = ri % 10;
        ri /= 10;
    }
    //处理好了这些数据,我们开始操作ヾ(=゚・゚=)ノ♪
    //首先,所有长度比len小的数都是在范围内的
    for(int i = len1 - 1; i > 0; i--){
        for(int j = 1; j <= 9; j++){
            res1 += dp[i][j];
        }
    }
    for(int i = len2 - 1; i > 0; i--){
        for(int j = 1; j <= 9; j++){
            res2 += dp[i][j];
        }
    }
    //然后,最高位小于所求数最高位的自然也包括
    for(int j = 1; j < n1[len1]; j++){
        res1 += dp[len1][j];
    }
    for(int j = 1; j < n2[len2]; j++){
        res2 += dp[len2][j];
    }
    //最后,如果最高位和所求数相等,就只好,每位数挨个比咯
    for(int i = len1 - 1; i > 0; i--){
        for(int j = 0; j <= n1[i] - 1; j ++){
            if(abs(j - n1[i+1]) >= 2){
                res1 += dp[i][j];
            }
        }
        if(abs(n1[i+1] - n1[i]) < 2){
            break;
        }
    }
    for(int i = len2 - 1; i > 0; i--){
        for(int j = 0; j  <= n2[i] - 1; j ++){
            if(abs(j - n2[i+1]) >= 2){
                res2 += dp[i][j];
            }
        }
        if(abs(n2[i+1] - n2[i]) < 2){
            break;
        }
    }
    cout<<res2 - res1<<endl;
    return 0;
}

七、树形dp

有附属关系的dp,主要是搜索方法不同于之前的线性dp,但逻辑之类的感觉还是比较像的,搜索再之前的二叉树中有提到一些方法,在这里是够用的~~(难题另说じò ぴé)~~

1、没有上司的舞会

题目描述

某大学有 n 个职员,编号为 1…n

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

输入格式

输入的第一行是一个整数 n

第 2 到第 (n+1) 行,每行一个整数,第 (i+1) 行的整数表示 i 号职员的快乐指数 ri

第 (n+2) 到第 2n 行,每行输入一对整数 l,k,代表 kl 的直接上司。

输出格式

输出一行一个整数代表最大的快乐指数。

输入输出样例
输入 #1
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出 #1
5
说明/提示
数据规模与约定

对于 100% 的数据,保证 1≤n≤6×103,−128≤ri≤127,1≤l,kn,且给出的关系一定是一棵树。

这道题的状态转移逻辑还是比较清晰,就是上司去的话,那总值就是下属不去的最好情况,上司不去的话,就模拟每个下属去或不去,看那种情况最好。

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
vector<int> son[6006];//存储每个人的下属
int w[6006];//存储每个人的快乐值
int root[6006];//如果为0代表是根
int rt = 0;
int dp[6006][2];//0代表他不来的最好情况,1代表他来的最好情况
void fun(int rt){
    dp[rt][0] = 0;
    dp[rt][1] = w[rt];
    for(int i = 0 ; i < son[rt].size(); i++){
        int s = son[rt][i];
        fun(s);
        dp[rt][0] += max(dp[s][0],dp[s][1]);
        dp[rt][1] += dp[s][0];
    }
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    n = r;
    for(int i =  1; i <= n; i++){
        w[i] = r;
    }
    for(int i = 1; i < n; i++){
        int x = r;
        int y = r;
        son[y].push_back(x);
        root[x] = 1;
    }
    for(int i = 1; i <= n; i++){
        if(root[i] == 0){
            rt = i;
            break;
        }
    }
    fun(rt);
    cout<<max(dp[rt][0],dp[rt][1]);
    return 0;
}

2、战略游戏

题目背景

Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。

题目描述

他要建立一个古城堡,城堡中的路形成一棵无根树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。

注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。

请你编一程序,给定一树,帮 Bob 计算出他需要放置最少的士兵。

输入格式

第一行一个整数 n,表示树中结点的数目。

第二行至第 n+1 行,每行描述每个结点信息,依次为:一个整数 i,代表该结点标号,一个自然数 k,代表后面有 k 条无向边与结点 i 相连。接下来 k 个整数,分别是每条边的另一个结点标号 r1,r2,⋯,rk,表示 i 与这些点间各有一条无向边相连。

对于一个n 个结点的树,结点标号在 0 到 n−1 之间,在输入数据中每条边只出现一次。保证输入是一棵树。

输出格式

输出文件仅包含一个整数,为所求的最少的士兵数目。

输入输出样例
输入 #1
4
0 1 1
1 2 2 3
2 0
3 0
输出 #1
1
说明/提示
数据规模与约定

对于全部的测试点,保证 1≤n≤1500。

这道题在树形dp方面和上一道题差不多,但注意他是一个无根树(??)什么是无根树?其实就是无环连通无向图(更加迷惑了ヾ(。 ̄□ ̄)ツ゜゜゜)啊哈这是百度这么说的,但这道题我们可以理解为一个无序的树,所以我们存储根节点的时候要存两遍,查找的时候注意别回头就行了(因为他是无环的)其他的操作和上道题差不多。

AC code
#include <iostream>
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
vector<int> son[1505];
int dp[1505][2];
int root = 0;
int fa = -1;
void fun(int root,int fa){
    dp[root][0] = 0;
    dp[root][1] = 1;
    for(int i = 0; i < son[root].size(); i++){
        int y = son[root][i];
        if(y == fa){
            continue;
        }
        fun(y,root);
        dp[root][0] += dp[y][1];
        dp[root][1] += min(dp[y][0],dp[y][1]);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    int n = r;
    for(int i = 0; i < n; i++){
        int t = r;
        int k = r;
        for(int j = 0; j < k; j++){
            int x = r;
            son[i].push_back(x);
            son[x].push_back(i);
        }
    }
    fun(root,fa);
    cout<<min(dp[0][1],dp[0][0])<<endl;
    return 0;
}

八、树上背包

我们之前有提到过背包问题,刚刚也看过了树形dp树上背包就是二者的结合,其实我们可以把问题拆成两部分,一部分是遍历,之前一个for循环就搞定了,这里我们需要用到树的遍历,然后剩下的其实就是背包问题了。

1、P2014 [CTSC1997]选课

题目描述

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少?

输入格式

第一行有两个整数 N , M 用空格隔开。( 1≤N≤300 , 1≤M≤300 )

接下来的 N 行,第 I+1 行包含两个整数 kisi, ki 表示第I门课的直接先修课,si 表示第I门课的学分。若ki=0 表示没有直接先修课(1≤kiN , 1≤si≤20)。

输出格式

只有一行,选 M 门课程的最大得分。

输入输出样例
输入 #1
7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2
输出 #1
13

这道题是比较经典的模板题,按照之前说的分成遍历和dp两部分处理即可

AC code
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
//阶乘
int factorial(int k){
    if(k <= 1){
        return 1;
    }
    else{
        return k * factorial(k - 1);
    }
}
int n,m;
int root = 0;
vector<int> son[303];//存儿子结点
int dp[303][303];
void fun(int root){
    for(int i = 0; i < son[root].size(); i++){
        int y = son[root][i];
        fun(y);
    }
    for(int i = 0; i < son[root].size(); i++){
        int y = son[root][i];
    for(int j = m+1; j > 0; j-- ){
        for(int k = 1; k < j; k++){
            dp[root][j] = max(dp[root][j],dp[root][j-k] + dp[y][k]);
            }
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    n = r;
    m = r;
    for(int i = 1; i <= n; i++){
        int x = r;
        int y = r;
        son[x].push_back(i);
        dp[i][1] = y;
    }
    fun(root);
    cout<<dp[0][m+1];
    return 0;
}

2、P1273 有线电视网

题目描述

某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。

从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。

现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。

写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。

输入格式

输入文件的第一行包含两个用空格隔开的整数N和M,其中2≤N≤3000,1≤M≤N-1,N为整个有线电视网的结点总数,M为用户终端的数量。

第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。

接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:

K A1 C1 A2 C2 … Ak Ck

K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。

输出格式

输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。

输入输出样例
输入 #1
5 3
2 2 2 5 3
2 3 2 4 3
3 4 2
输出 #1
2
说明/提示

样例解释

img

如图所示,共有五个结点。结点①为根结点,即现场直播站,②为一个中转站,③④⑤为用户端,共M个,编号从N-M+1到N,他们为观看比赛分别准备的钱数为3、4、2,从结点①可以传送信号到结点②,费用为2,也可以传送信号到结点⑤,费用为3(第二行数据所示),从结点②可以传输信号到结点③,费用为2。也可传输信号到结点④,费用为3(第三行数据所示),如果要让所有用户(③④⑤)都能看上比赛,则信号传输的总费用为:

2+3+2+3=10,大于用户愿意支付的总费用3+4+2=9,有线电视网就亏本了,而只让③④两个用户看比赛就不亏本了。

这道题和其他不一样的地方在于要动态规划的是包含叶子结点的个数,所以我们需要一个新的参数去统计每种情况下包含的叶子结点个数,dp【i】【j】数组里面存以i为根节点包含j个叶子结点的费用。遍历和背包这两个方向和上一道题基本一样。

AC code
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
//并查集
int f[1];
int found(int k){
    if(f[k] == k){
        return k;
    }
    return f[k] = found(f[k]);
}
//阶乘
int factorial(int k){
    if(k <= 1){
        return 1;
    }
    else{
        return k * factorial(k - 1);
    }
}
vector<int> son[3003];
int dp[3003][3003];
int cnt[3003];
int v[3003];
int n,m;
int root = 1;
int fun(int root){
    if(son[root].size() == 0){
        dp[root][1] = v[root];
        return 1;
    }
    int sum = 0,temp;
    for(int i = 0; i < son[root].size(); i++){
        int y = son[root][i];
        temp = fun(y);
        sum += temp;
        for(int j = sum; j > 0; j--){
            for(int k = 1; k <= temp; k++){
                if(j-k >= 0){
                    dp[root][j] = max(dp[root][j],dp[root][j-k] + dp[y][k] - cnt[y]);
                }
            }
        }
    }
    return sum;
}
int main()
{
    ios::sync_with_stdio(false);
    n = r;
    m = r;
    for(int i = 1 ;i <= n - m; i++){
        int k = r;
        for(int j = 1; j <= k; j++){
            int a = r;
            int c = r;
            son[i].push_back(a);
            cnt[a] = c;
        }
    }
    for(int i = n - m + 1; i <= n; i++){
        int a = r;
        v[i] = a;
    }
    memset(dp,~0x3f,sizeof(dp));
    for(int i = 1;i <= n; i++){
        dp[i][0] = 0;
    }
    fun(root);
    for(int i = m; i > 0; i--){
        if(dp[1][i] >= 0){
            cout<<i<<endl;
            break;
        }
    }
    return 0;
}

九、小结

这些例题主要帮助我们认识动态规划做基础的类型,当然动态规划远不止于此,后面如果有机会再对一些类型的动态规划再写一些深入的例题,这周的内容就到这里了,如果能看完的话~~(是真的强ヾ§  ̄▽)ゞ2333333)~~,相信你对动态规划能有更深刻的了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值