引言:
一直都想学
d
p
dp
dp,只是由于种种原因,一直都没有沉下心来好好的,系统的,认真的学一下,不过现在怎么开始学了呢?这得从一直蝙蝠讲起 。。。(以下省略一万字)
由于
d
p
dp
dp的东西实在是太多啦,学起来也不是很好理解,每天学的东西也很有限,所以就专门用一个
b
l
o
g
blog
blog来记录先从现在开始学习的
d
p
dp
dp内容吧,包括
d
p
dp
dp经典模型,遇到的
d
p
dp
dp题目
.
.
.
...
...笔者能力有限,所写内容难免会有错误,纰漏等,如果出现,还望读者指出,不胜感激。
在我的理解中, d p dp dp并不是像 D i j k s t r a , K r u s k a l Dijkstra,Kruskal Dijkstra,Kruskal那样是一种具体的算法,有具体的板子可以套的,他是像贪心那样,是一种算法思想,是一种 “ “ “优雅的暴力 ” ” ”
直接讲 d p dp dp可能不是很理解,我在这里先用几个引例来来介绍 d p dp dp的思想。
引例1:斐波那契数列。
众所周知,斐波那契数列的递推公式是
f
[
n
]
=
f
[
n
−
1
]
+
f
[
n
−
2
]
f[n] = f[n-1] + f[n-2]
f[n]=f[n−1]+f[n−2],且
f
[
1
]
=
f
[
2
]
=
1
f[1] = f[2] = 1
f[1]=f[2]=1。
所以学过递归的同学可以直接写出求解斐波那契数列第n项的代码,如下:
int f(int x) {
if(x == 1 || x == 2) return 1;
else return f(x-1) + f(x-2);
}
毫无疑问,这个代码是对的,但是,这个2代码时间复杂度有多高呢?我们俩可以画一下这个函数执行时的递归树:
就只画了三层,下面的就没画啦。
我们可以看到,每计算一个n我们要计算接近
2
n
2^{n}
2n次,所以这个算法是一个指数级的复杂度,一旦数据稍微大一点,肯定就过不去啦。
那我们就要想一下优化一点的方法,怎么优化呢?
我们观察一下上面的递归树,我们可以看到,有很多节点被重复计算啦,那我们就可以想到,我能不能用一个类似字典的操作,将已经计算出来的节点数给保存在字典里,那样我们每次要第n项的斐波那契数时,我们先在字典里找一下这个数是否已经计算出来啦,如果计算出来的话,那我们直接返回值不就好啦吗,这样不就可以避免很多的重复的计算,节省很多时间嘛。那代码该怎么写呢?
int vis[maxn]; //相当于上面说的那个"字典"
int f(int x) {
if(vis[x]) return vis[x]; //如果第x项已经计算出来啦,直接返回
if(x == 1 || x == 2) return vis[x] = 1;
else return vis[x] = f(x-1) + f(x-2); //记录第x项的值
}
这样,这个函数的递归树如下:
少画了好多节点就是因为这些节点已经被计算过啦,不需要再计算啦,是不是感觉这棵树少了不止一半!!!而这个算法的时间复杂度是很有些的,接近O(n),而这种方法一般被称为记忆化搜索!!!
其实我们还有一种更简单的写法,如下:
int f[maxn];
f[1] = f[2] = 1;
for(int i = 3 ; i <= n ; i++) {
f[i] = f[i-1] + f[i-2];
}
是不是简洁易懂很多!!!而且这应该是大部分人比较喜欢的一种写法。
引例2:Floyd算法。
最短路算法作为图论入门的算法,相信大部分同学都不会陌生,我这里主要引用一下Floyd算法。
首先可以看一下Floyd算法的代码:
int dist[maxn][maxn];
for(int i = 1 ; i <= n ; i++) {
for(int j = 1 ; j <= n ; j++) {
for(int k = 1 ; k <= n ; k++) {
dist[i][j] = min(dist[i][j] , dist[i][k] + dist[k][j]);
}
}
}
这其实就是一种非常非常典型的暴力 啦!!!
那为什么要将这个呢?我们可以观察这个方程
d
i
s
t
[
i
]
[
j
]
=
m
i
n
(
d
i
s
t
[
i
]
[
j
]
,
d
i
s
t
[
i
]
[
k
]
+
d
i
s
t
[
k
]
[
j
]
)
dist[i][j] = min(dist[i][j] , dist[i][k] + dist[k][j])
dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j])
这个在
d
p
dp
dp里经常遇到,我们叫他状态转移方程。何为状态?
引例3:取石子问题。
给你n个石子,每次可以去一个或者两个,取走最后一个石子的赢,问你最后是先手赢还是后手赢。熟悉博弈的同学应该可以直接看出来这个结论,但我们可以用这个简单的例子来理解何为状态。比如现在还剩
i
i
i个石子,我们定义
f
[
i
]
=
0
f[i] = 0
f[i]=0表示先手输这种状态,
f
[
i
]
=
1
f[i] = 1
f[i]=1表示先手赢这种状态。这样是不是对状态有点理解啦。由于每个人每次只能拿一个或者两个石子,那么
f
[
i
]
f[i]
f[i]这种状态是不是与
f
[
i
−
1
]
f[i-1]
f[i−1]或者
f
[
i
−
2
]
f[i-2]
f[i−2]有关呢?答案是肯定的。因为我作为先手拿完一个或者两个以后,后手就变成了先手,那如果
f
[
i
−
1
]
=
=
0
∣
∣
f
[
i
−
2
]
=
=
0
f[i-1] == 0 || f[i-2] == 0
f[i−1]==0∣∣f[i−2]==0,会发生什么呢?还记得我们定义的状态吗?
f
[
i
]
=
=
0
f[i] == 0
f[i]==0表示先手输,所以如果
f
[
i
−
1
]
=
=
0
∣
∣
f
[
i
−
2
]
=
=
0
f[i-1] == 0 || f[i-2] == 0
f[i−1]==0∣∣f[i−2]==0,不就相当于先手拿完以后后手变先手(注意断句),直接输啦,那原来的先手不就赢了吗!!!所以此时
f
[
i
]
=
=
1
f[i] == 1
f[i]==1。所以代码实现就是这样:
int f[maxn];
f[1] = f[2] = 1;
for(int i = 3 ; i <= n ; i++) {
if(f[i-1] == 0 || f[i-2] == 0) f[i] = 1;
else f[i] = 0;
}
(其实这个规律就是 n n n是三的倍数先手输。)
说了这么多,好像还是不知道 d p dp dp是什么,那接下来进入正题吧!!!
一般
d
p
dp
dp问题都要满足最优子结构和无后效性这两个原则;
而
d
p
dp
dp的核心就是状态的设计和重叠的子问题结构;
d
p
dp
dp的核心思想是从最简单的子问题开始,逐步扩大子问题规模。
最优子结构:每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到;
无后效性:指的是前面出现的以及前面状态的转移对后面的状态不会有影响。
这里引用这位大佬的知乎里的一段话解释一下什么是最优子结构:
例如有向图的无权最短路径问题与无权最长路径问题,前者有最优子结构,但后者作为NP完全问题是没有最优子结构的。这是由于最长简单子路径问题是相关的,一个子问题使用过的顶点是无法被另一个子问题使用的;而最短简单子路径问题之间不共享资源,是无关的(independent),因为假如有一个顶点同时出现在两条最短子路径上,就可以通过这个顶点构造出更短的路径,原来的两条子路径就不是最优子路径了。
那什么是无后效性呢?可以再看看上面的引例3的那个取石子问题,我们在状态转移的时候只关心 f [ i − 1 ] f[i-1] f[i−1]和 f [ i − 2 ] f[i-2] f[i−2]是不是等于0,但我们并不关心 f [ i − 1 ] f[i-1] f[i−1]和 f [ i − 2 ] f[i-2] f[i−2]为什么不等于0,对不对?有些同学可能就会说啦,我为什么要关心这个呀?对呀,为什么呀?因为 i i i前面出现的状态以及状态转移对 i i i这个状态没有影响。。。等等,这句话是不是感觉很熟悉,感觉在哪看到过,没错,就是前面我对无后效性这句话的解释!!!
下面还是应用这位大佬的blog里的几段话说一下 d p dp dp中的一些概念一起 d p dp dp和其他的相似的算法的比较
(1)动态规划包含三个重要的概念:
-最优子结构
-边界
-状态转移方程
(2)解题的一般步骤是:
1.找出最优解的性质,刻画其结构特征和最优子结构特征;
2.递归地定义最优值,刻画原问题解与子问题解间的关系;
3.以自底向上的方式计算出各个子问题、原问题的最优值,并避免子问题的重复计算;
4.根据计算最优值时得到的信息,构造最优解。
(3)使用动态规划特征:
1.求一个问题的最优解
2.大问题可以分解为子问题,子问题还有重叠的更小的子问题
3.整体问题最优解取决于子问题的最优解(状态转移方程)
4.从上往下分析问题,从下往上解决问题
5.讨论底层的边界问题
(4) d p dp dp和其他算法的比较
1.每个阶段只有一个状态->递推;
2.每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
3.每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
4.每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。
5.每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到,这个性质叫做最优子结构;
6.而不管之前这个状态是如何得到的,个性质叫做无后效性。
说了那么多,都不过是一些云里雾里的概念而已,如何在题目中运用呢?接下来就该将题目啦!!!
d p dp dp经典模型
1、背包模型:
(1)01背包
问题:给你
n
n
n个物品,和一个容量为W的背包,每个物品有重量
w
[
i
]
w[i]
w[i]和价值
v
[
i
]
v[i]
v[i],每个物品只有一个(就是每个物品要么取,要么不取),问在不超过背包容量的前提下可以获得的最大价值。
思路:有些人可能考虑用贪心来做,就去性价比最高的不就好啦。(即取
v
[
i
]
/
w
[
i
]
v[i]/w[i]
v[i]/w[i]最大的)看似及其,灰常的有道理。手动滑稽 01背包用贪心肯定是不行的,为什么呢?还是引用这位大佬的blog里的一段话来解释一下:
它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。
所以我们考虑用
d
p
dp
dp来求解,那该如何做呢?我们可以用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]来表示取了前
i
i
i个物品以后装容量为
j
j
j的背包所能得到的最大价值。
那对于每一件物品,如果我们不拿,那
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j] = dp[i-1][j]
dp[i][j]=dp[i−1][j]
如果我们拿,那
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
dp[i][j] = dp[i-1][j-w[i]] + v[i]
dp[i][j]=dp[i−1][j−w[i]]+v[i]
这样我们就可以知道
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
+
d
[
i
−
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
dp[i][j] = max(dp[i-1][j] + d[i-1][j-w[i]]+v[i]
dp[i][j]=max(dp[i−1][j]+d[i−1][j−w[i]]+v[i]
那么我们就可以写代码来解决这个问题,如下:
for(int i = 1 ; i <= n ; i++) {
dp[i][0] = 0;
for(int j = w[i] ; j <= W ; j++) {
dp[i][j] = max(dp[i-1][j] , dp[i-1][j-w[i]] + v[i]);
}
}
也许这个方程在时间上确实无法再继续优化啦,但我们可以用滚动数组(不明白滚动数组的建议先去学一下)来优化空间。
因为
d
p
[
i
]
dp[i]
dp[i]只依赖于
d
p
[
i
−
1
]
dp[i-1]
dp[i−1],所以我们可以将数组降一维,这样就可以定义新的状态
d
p
[
j
]
dp[j]
dp[j]表示容量为
j
j
j的背包可以容纳的价值最大的物品。
那新的状态转移方程就是
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
[
i
]
]
+
v
[
i
]
dp[j] = max(dp[j] , dp[j-w[i]] + v[i]
dp[j]=max(dp[j],dp[j−w[i]]+v[i],但在写代码的时候要注意一点就是这个时候第二维要枚举
W
−
>
w
[
i
]
W -> w[i]
W−>w[i],为什么呢?还是引用这位大佬的blog:
如果,我们使用正序循环,从w[i]到W,那么我们可能出现这种情况:
f[j]被f[j-wi]+vi更新过,当我们j增加到j+w[i]时,f[j+w[i]]又有可能被f[j]+vi更新,而同时它们都处于阶段i,也就是说,我们在一个阶段内的两个状态间发生了转移,相当于第i个物品被使用了多次(如果后面又更新了),不符合0/1背包的要求
这样就可以写出优化后的Code如下:
int dp[maxn];
dp[0] = 0;
for(int i = 1 ; i <= n ; i++) {
for(int j = W ; j >= w[i] ; j--) {
dp[j] = max(dp[j] , dp[j - w[i]] + v[i]);
}
}
(2)完全背包
问题:给你
n
n
n个物品,和一个容量为W的背包,每个物品有重量
w
[
i
]
w[i]
w[i]和价值
v
[
i
]
v[i]
v[i],每个物品有无限个,问在不超过背包容量的前提下可以获得的最大价值。
思路:还是和01背包那样,我们应该可以知道这一题用贪心做不了,所以还是考虑
d
p
dp
dp。我们还是用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示取钱
i
i
i件物品,容量为
j
j
j的背包所能获得的最大价值。
那状态转移方程怎么写呢?
对于每一件物品,我们可以:
1、不取,那么
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j] = dp[i-1][j]
dp[i][j]=dp[i−1][j];
2、取1个,那么
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
dp[i][j] = dp[i-1][j-w[i]] + v[i]
dp[i][j]=dp[i−1][j−w[i]]+v[i]
3、取2个,那么
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
2
∗
w
[
i
]
]
+
2
∗
v
[
i
]
dp[i][j] = dp[i-1][j-2*w[i]] + 2 * v[i]
dp[i][j]=dp[i−1][j−2∗w[i]]+2∗v[i]
.
.
.
...
...
k、取k个,那么
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
k
∗
w
[
i
]
]
+
k
∗
v
[
i
]
dp[i][j] = dp[i-1][j-k*w[i]]+k*v[i]
dp[i][j]=dp[i−1][j−k∗w[i]]+k∗v[i]
所以,
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
k
∗
w
[
i
]
]
+
k
∗
v
[
i
]
)
dp[i][j] = max(dp[i-1][j-k*w[i]]+k*v[i])
dp[i][j]=max(dp[i−1][j−k∗w[i]]+k∗v[i])
所以我们就可以写出完全背包的Code啦:
int dp[maxn][maxn];
for(int i = 1 ; i <= n ; i++) {
for(int j = w[i] ; j <= W ; j++) {
for(int k = 0 ; k * w[i] <= j ; k ++) {
dp[i][j] = max(dp[i][j] , dp[i-1][j-k*w[i]] + k*v[i]);
}
}
}
还是和01背包类似,完全背包空间也是可以优化到一维的,还记得我们讲01背包优化的时候讲到第二重
f
o
r
for
for循环要枚举
W
−
>
w
[
i
]
W->w[i]
W−>w[i]嘛?还记得为什么嘛?就是枚举
w
[
i
]
−
>
W
w[i] -> W
w[i]−>W的话一个物品可能会被拿多次,而01背包每个物品只能拿1次。
等等,每个物品可以被拿多次?这不正好符合多重背包的要求吗?尽然会有这样的巧合,所以我们写出一维的Code,就是这样:
int dp[maxn];
dp[0] = 0;
for(int i = 1 ; i <= n ; i++) {
for(int j = w[i] ; j <= W ; j++) {
dp[j] = max(dp[j] , dp[j - w[i]] + v[i]);
}
}
(3)多重背包
问题:给你
n
n
n个物品,和一个容量为W的背包,每个物品有重量
w
[
i
]
w[i]
w[i]和价值
v
[
i
]
v[i]
v[i],每个物品有
m
[
i
]
m[i]
m[i]个,问在不超过背包容量的前提下可以获得的最大价值。
思路:讲过上面那个多重背包未优化版之后,相信笔者看到题目就应该可以想到多重背包的为优化版代码应该长这样:
int dp[maxn][maxn];
for(int i = 1 ; i <= n ; i++) {
for(int j = w[i] ; j <= W ; j++) {
for(int k = 0 ; k * w[i] <= j && k <= m[i] ; k ++) {
dp[i][j] = max(dp[i][j] , dp[i-1][j-k*w[i]] + k*v[i]);
}
}
}
那如何优化呢?多重背包的优化相当复杂(一定是我太菜啦)。他有两个优化方法,其中低配版是二进制拆分,高配版是单调队列优化。
二进制拆分是怎么回事呢?
首先,我们考虑一种最笨的方法,我们将所有的物品都看
m
[
i
]
m[i]
m[i]个,就刚开始的问题是一个物品有
m
[
i
]
m[i]
m[i]个,我们现在可以看成我们有
m
[
i
]
m[i]
m[i]个不同的物品,只不过这
m
[
i
]
m[i]
m[i]个物品非常非常碰巧有相同的重量和价值罢啦。那这是不是就是一个01背包问题啦。但这种方法是很低效的,因为这样都可以过的话,我直接暴力,凭啥不能过。所以我们要考虑一种更聪明的拆法。现在我们回归问题的本质,就是我们为什么要把每个物品拆成
m
[
i
]
m[i]
m[i]份?因为这种拆法我们可以表示出
m
[
i
]
m[i]
m[i]以内的所有数。那这个问题的一个具体化就收给你任意一个数
n
n
n,我们只要找到任意
m
m
m个数,这
m
m
m个数可以表示出
n
n
n以内的所有数!!!熟悉二进制的同学应该马上两眼一亮,马上明白啦,只要把
n
n
n进行二级制拆分就好啦。比如数字7,我么拆成1 2 4,这三个数可以表示出7以内的所有数!不信?你可以从1到7试试看,看看能不能找到一个数是1 2 4表示不出来的。
那这种问题我们就可以对每个
m
[
i
]
m[i]
m[i]进行二进制拆分,之后跑一个01背包就更好拉。具体代码如下:
#include <map>
#include <cmath>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define pii pair<int,int>
const int maxn = 6e3 + 7;
const int INF = 0x3f3f3f3f;
struct Good {
int v,w;
Good(int _v = 0 , int _w = 0) : v(_v) , w(_w) {}
};
vector<Good>good;
int dp[maxn];
int main() {
int n,W;
scanf("%d%d",&n,&W);
for(int i = 1 ; i <= n ; i++) {
int w,v,m;
scanf("%d%d%d",&w,&v,&m);
//二进制拆分
for(int j = 1 ; j <= m ; j *= 2) {
good.push_back(Good(j*v,j*w));
m -= j;
}
if(m != 0) {
good.push_back(Good(m*v,m*w));
}
}
//01背包
for(int i = 0 ; i < good.size() ; i++) {
for(int j = W ; j >= good[i].w ; j--) {
dp[j] = max(dp[j] , dp[j - good[i].w] + good[i].v);
}
}
printf("%d\n",dp[W]);
return 0;
}
单调队列如何优化呢?
大家可以看这个这个大佬的视频28min - 40min我真的讲不明白QAQ。
给大家安利一个优化后的代码吧:
#include <map>
#include <cmath>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
const int maxn = 2e5 + 7;
int vi[maxn],wi[maxn],mi[maxn];
struct node {
int id,val;
node (int _val = 0 , int _id = 0) : val(_val) , id(_id) {}
};
int dp[maxn];
node dq[maxn];
int main() {
int n,W;
while(~scanf("%d%d",&n,&W)) {
for(int i = 1 ; i <= n ; i++) {
scanf("%d%d%d",&wi[i],&vi[i],&mi[i]);
}
for(int i = 0 ; i <= W ; i++) dp[i] = 0;
for(int i = 1 ; i <= n ; i++) { ///枚举n个物品
int v = vi[i] , w = wi[i] , m = mi[i];
for(int d = 0 ; d < w ; d++) { ///枚举余数
int head = 0 , tail = -1; ///清空队列
for(int j = 0 ; d + j * w <= W ; j++) {///枚举j
if(j - dq[head].id > m) head++;
while(head <= tail && dq[tail].val <= dp[j * w + d] - j * v) tail--;
dq[++tail] = node(dp[j * w + d] - j * v , j);
dp[j * w + d] = dq[head].val + j * v;
}
}
}
printf("%d\n",dp[W]);
}
return 0;
}
(4)分组背包:
问题:给你
n
n
n组物品,每组物品有
c
[
i
]
c[i]
c[i]个,每个物品有自己的价值
v
[
i
]
[
j
]
v[i][j]
v[i][j]和重量
w
[
i
]
[
j
]
w[i][j]
w[i][j],背包得容积是
W
W
W,每组物品最多选一个,问在不超过背包容积得情况下所能装的物品得最大得价值。
思路:首先分析题意,题目说了每组物品最多只能选择一个,要么就是这组物品选一个,要么就是整组物品都不选,那不就是相当于一个01背包嘛?我们假设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示在前
i
i
i组中选了重量为
j
j
j的物品所能获得的最大价值。
如果我们不选第
i
i
i组物品,那么
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j] = dp[i-1][j]
dp[i][j]=dp[i−1][j]
如果我们选了第
i
i
i组物品,那么
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
w
[
i
]
[
k
]
]
+
v
[
i
]
[
k
]
)
,
1
<
=
k
<
=
c
[
i
]
dp[i][j] = max(dp[i-1][j-w[i][k]] + v[i][k]),1 <= k <= c[i]
dp[i][j]=max(dp[i−1][j−w[i][k]]+v[i][k]),1<=k<=c[i]
参照前面的问题,我们还是可以将空间降到一维,那么
d
p
[
j
]
=
m
a
x
(
d
p
[
j
−
w
[
i
]
[
k
]
]
+
v
[
i
]
[
k
]
)
0
<
=
k
<
=
c
[
i
]
dp[j] = max(dp[j-w[i][k]]+v[i][k]) 0 <= k <= c[i]
dp[j]=max(dp[j−w[i][k]]+v[i][k])0<=k<=c[i]
那我们就可以写代码啦:
for(int i = 1 ; i <= n ; i++) {
for(int j = W ; j >= 0 ; j--) {
for(int k = 1 ; k <= c[i] ; k++) {
if(j - w[i][k] >= 0) {
dp[j] = max(dp[j],dp[j-w[i][k]] + v[i][k]);
}
}
}
}
2、最长公共子序列(
L
C
S
LCS
LCS):
问题:给你两个序列
s
1
s1
s1和
s
2
s2
s2,问你这两个序列的最长公共子序列(感觉和不讲没区别),可以看一下这道题
思路:还是先确定状态,我们用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]来表示
s
1
s1
s1前
i
i
i个字符和
s
2
s2
s2前
j
j
j个字符的
L
C
S
LCS
LCS
我们让
s
1
s1
s1和
s
2
s2
s2的序号都从1开始(个人习惯)
这样如果
s
1
[
i
]
=
=
s
2
[
j
]
,
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
1
]
+
1
;
s1[i] == s2[j],dp[i][j] = dp[i-1][j-1] + 1;
s1[i]==s2[j],dp[i][j]=dp[i−1][j−1]+1;
如果
s
1
[
i
]
!
=
s
2
[
j
]
,
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
;
s1[i] != s2[j],dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
s1[i]!=s2[j],dp[i][j]=max(dp[i−1][j],dp[i][j−1]);
这样也可以说一下为什么
s
1
s1
s1和
s
2
s2
s2的序号要从1开始呢?(除了个人习惯这个原因之外)
因为如果从0开始且
s
1
[
0
]
=
=
s
2
[
0
]
,
d
p
[
0
]
[
0
]
=
d
p
[
−
1
]
[
−
1
]
+
1...
s1[0] == s2[0],dp[0][0] = dp[-1][-1] + 1...
s1[0]==s2[0],dp[0][0]=dp[−1][−1]+1...
这样时间复杂度就是
O
(
s
t
r
l
e
n
(
s
1
)
∗
s
t
r
l
e
n
(
s
2
)
)
;
O(strlen(s1) * strlen(s2));
O(strlen(s1)∗strlen(s2));
代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const int maxn = 1010;
const LL INF = 1e12;
const LL mod = 1e9 + 7;
int dp[maxn][maxn];
char s1[maxn],s2[maxn];
int main() {
while(~scanf("%s%s",s1+1,s2+1)) {
int len1 = strlen(s1+1);
int len2 = strlen(s2+1);
for(int i = 1 ; i <= len1 ; i++) {
for(int j = 1 ; j <= len2 ; j++) {
if(s1[i] == s2[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j] , dp[i][j-1]);
}
}
printf("%d\n",dp[len1][len2]);
}
return 0;
}
3、最长上升子序列(
L
I
S
LIS
LIS)
问题:给你
n
n
n个数,问你这
n
n
n个数严格单调递增的序列最长的长度。
思路:我们用 d p [ i ] dp[i] dp[i]表示前 i i i位 L I S LIS LIS的长度。那我们到第 i i i位时,我们只需要找到第 i i i位之前的比 a [ i ] a[i] a[i]小的对应得 d p dp dp值的最大值+1便是 d p [ i ] dp[i] dp[i]的值。很明显,这样的算法时间复杂度时 O ( n 2 ) O(n^{2}) O(n2)的,对于一般的 n n n比较小的数据,还是可以水过去的,比如上面那道题。代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const int maxn = 1010;
const LL INF = 1e12;
const LL mod = 1e9 + 7;
int dp[maxn];
int a[maxn];
int main() {
int n;
while(~scanf("%d",&n)) {
for(int i = 1 ; i <= n ; i++) {
scanf("%d",&a[i]);
}
for(int i = 1 ; i <= n ; i++) {
dp[i] = 1;
for(int j = 1 ; j < i ; j++) {
if(a[i] > a[j]) {
dp[i] = max(dp[i],dp[j] + 1);
}
}
}
int ans = 0;
for(int i = 1 ; i <= n ; i++) {
ans = max(ans,dp[i]);
}
printf("%d\n",ans);
}
return 0;
}
但是其实求 L I S LIS LIS还有一种 O ( n l o g n ) O(nlogn) O(nlogn)的算法,是基于二分实现的,怎么做呢?我们刚刚那种 n 2 n^{2} n2的算法主要是第二层 f o r for for循环花了太多时间,那又有没办法将这第二层 f o r for for循环给替代掉呢?肯定是可以的,我们用 b [ i ] b[i] b[i]来表示前 i i i个数里面最大的那个,然后每次遇到一个数,我们在 b [ ] b[] b[]数组里找最后一个比他小的数,其下标记为 p o s pos pos,那我们可以直接更新 b [ p o s ] = a [ i ] b[pos] = a[i] b[pos]=a[i],因为 b [ ] b[] b[]数组是单调不减的,所以可以直接用二分来求,这样复杂度就降到 O ( n l o g n ) O(nlogn) O(nlogn)。具体代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const int maxn = 1010;
const LL INF = 1e12;
const LL mod = 1e9 + 7;
int b[maxn];
int a[maxn];
int main() {
int n;
while(~scanf("%d",&n)) {
for(int i = 1 ; i <= n ; i++) {
scanf("%d",&a[i]);
}
int cnt = 0;
b[0] = -1;
for(int i = 1 ; i <= n ; i++) {
if(b[cnt] < a[i]) {
b[++cnt] = a[i];
continue;
}
int l = 1 , r = cnt;
int res = -1;
while(l <= r) {
int mid = (l + r + 1) >> 1;
if(b[mid] > a[i]) {
res = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
b[res] = a[i];
}
printf("%d\n",cnt);
}
return 0;
}
4、数塔模型:
数塔模型就是形如以下的一个DAG(有向无环图):
每个节点有自己的权值,一般的题目都会问你按照箭头的方向走,从顶点走到底,每到一个节点可以加上这个节点的权值,问你最后得到的值得最大值。
思路:我们可以考虑用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示到第
i
i
i层第
j
j
j个节点获得的权值得最大值。那我们可以们直观的得出
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
1
]
)
+
a
[
i
]
[
j
]
dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]) + a[i][j]
dp[i][j]=max(dp[i−1][j],dp[i−1][j−1])+a[i][j]
(注意特判一下
j
=
=
1
∣
∣
j
=
=
i
j == 1 || j == i
j==1∣∣j==i得边界情况!)
讲一道例题吧。
题意: V o v a Vova Vova一天要睡 n n n次觉,每次睡一天,一天 h h h个小时,如果 V o c a Voca Voca下 l l l到 r r r得时间点入睡,那就说这次的睡眠是 g o o d good good,每次 V o c a Voca Voca可以选择在 a [ i ] a[i] a[i]入睡或者在 a [ i ] − 1 a[i]-1 a[i]−1入睡,问 g o o d good good睡眠次数得最大值。可能看题意不是很明白这道题是什么意思,我们来看一下样例:
input
7 24 21 23
16 17 14 20 20 11 22
output
3
这个3是怎么来的呢?(引用一下出题人得解释)
The story starts from t=0.
Then Vova goes to sleep after a1−1 hours, now the time is 15. This time is not good.
Then Vova goes to sleep after a2−1 hours, now the time is 15+16=7. This time is also not good.
Then Vova goes to sleep after a3 hours, now the time is 7+14=21. This time is good.
Then Vova goes to sleep after a4−1 hours, now the time is 21+19=16. This time is not good.
Then Vova goes to sleep after a5 hours, now the time is 16+20=12. This time is not good.
Then Vova goes to sleep after a6 hours, now the time is 12+11=23. This time is good.
Then Vova goes to sleep after a7 hours, now the time is 23+22=21. This time is also good.
可能看到这里还是一脸茫然,所以我们先把这个样例得到得部分的可能列出来(因为整个列出来工作量太大QAQ)
这是所有相同的节点合并到了一起以后得图(就像上面斐波那契数列递归树吧相同的节点合并到一起)这是不是就是一个刚刚说的数塔问题啦!!!
具体代码如下:
#include <map>
#include <cmath>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
const LL maxn = 2020;
int n,h,l,r;
int a[maxn];
int dp[maxn][maxn];
int b[maxn][maxn];
int main() {
while(~scanf("%d%d%d%d",&n,&h,&l,&r)) {
for(int i = 1 ; i <= n ; i++) {
scanf("%d",&a[i]);
}
memset(dp,0,sizeof(dp));
for(int i = 1 ; i <= n + 1 ; i++) {
for(int j = 1 ; j <= i ; j++) {
//计算数塔个节点得值
if(j != i) b[i][j] = (b[i-1][j] + a[i-1] - 1) % h;
else b[i][j] = (b[i-1][j-1] + a[i-1]) % h;
//因为第一个节点为0,l,r得范围可能为0,这样导致答案多计算了一次
if(i == 1 && j == 1) continue;
int res = 0;
if(b[i][j] >= l && b[i][j] <= r) res = 1;
//dp转移方程,注意考虑边界
if(j == 1) dp[i][j] = dp[i-1][j] + res;
else if(j == i) dp[i][j] = dp[i-1][j-1] + res;
else dp[i][j] = max(dp[i-1][j-1],dp[i-1][j]) + res;
}
}
//扫一遍最底层,获得答案
int ans = 0;
for(int i = 1 ; i <= n + 1 ; i++) {
ans = max(ans,dp[n+1][i]);
}
printf("%d\n",ans);
}
return 0;
}
5、树形
d
p
dp
dp
树形
d
p
dp
dp,顾名思义,就是在树上进行
d
p
dp
dp。这不是废话嘛 哈哈哈哈,我就用一道题来带各位入门树形
d
p
dp
dp吧。
题意:给你 n n n个点, m m m条双向边,每条边都有边权,再给你一格源点 s s s,定义删边的代价为边权,问你删掉部分边后使得所有度数为1的点都不能和源点 s s s相通的最小代价。
思路:首先分析这道题,我们注意到题目给的数据范围
m
=
n
−
1
m = n - 1
m=n−1,所以呢,题目给的图其实就是一颗树!!!而度数为1的结点就是这棵树的叶子节点。所以题目就装化成给你一颗以
s
s
s为根的树,让你删掉一些边,使得所有的叶子结点都无法到达根节点的最小代价。
我们按照
d
p
dp
dp的思路来考虑这个问题,用
d
p
[
u
]
dp[u]
dp[u]来表示以
u
u
u为根节点的子树删边所需要的最小代价。那我们考虑最一般的情况,如下图所示:
这样
d
p
[
u
]
=
d
i
s
[
u
]
[
v
]
,
d
i
s
[
u
]
[
v
]
dp[u] = dis[u][v],dis[u][v]
dp[u]=dis[u][v],dis[u][v]表示
u
u
u到
v
v
v的边权。
如果我们再加一格叶子节点呢?如下图:
那么我们可以得到
d
p
[
u
]
=
d
i
s
[
u
]
[
v
1
]
+
d
i
s
[
u
]
[
v
2
]
dp[u] = dis[u][v1]+dis[u][v2]
dp[u]=dis[u][v1]+dis[u][v2];
我们把图弄得在复杂一点,如下图所示:
那么未来让
v
1
,
v
2
v1,v2
v1,v2不能到达
u
u
u,我们有两种决策:
1、删掉
v
−
>
v
1
,
v
−
>
v
2
v->v1,v->v2
v−>v1,v−>v2这两条边,那么
d
p
[
u
]
=
d
p
[
v
]
dp[u] = dp[v]
dp[u]=dp[v]
2、删掉
u
−
>
v
u->v
u−>v这条边,那么
d
p
[
u
]
=
d
i
s
[
u
]
[
v
]
dp[u] = dis[u][v]
dp[u]=dis[u][v]
所以
d
p
[
u
]
=
m
i
n
(
d
p
[
v
]
,
d
i
s
[
u
]
[
v
]
)
dp[u] = min(dp[v],dis[u][v])
dp[u]=min(dp[v],dis[u][v]);
所以,是不是就明白了?这一题的状态转移方程就是
d
p
[
u
]
+
=
m
i
n
(
d
i
s
[
u
]
[
v
]
,
d
p
[
v
]
)
dp[u] += min(dis[u][v],dp[v])
dp[u]+=min(dis[u][v],dp[v])
然后要注意的就是因为是求最小值,那么每个叶子节点的
d
p
dp
dp初值要设为
I
N
F
INF
INF。
如何找叶子节点呢?出度为一的结点就是叶子结点。
那这一题就解决啦,具体实现详见代码:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define pii pair<int,int>
#define sd(x) scanf("%d",&x)
#define slld(x) scanf("%lld",&x)
#define pd(x) printf("%d\n",x)
#define plld(x) printf("%lld\n",x)
#define rep(i,a,b) for(int i = a ; i <= b ; i++)
#define per(i,a,b) for(int i = b ; i >= a ; i--)
#define mem(a) memset(a,0,sizeof(a))
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const LL INF = 1e18;
const int mod = 2010;
const int maxn = 2e5 + 7;
int head[maxn];
struct Edge {
int to,next;
LL w;
}edge[maxn];
int tot;
void init() {
memset(head,-1,sizeof(head));
tot = 0;
}
void addedge(int u,int v,LL w) {
edge[tot].w = w;
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
int vis[maxn];
LL dp[maxn];
int n,m,s;
void dfs(int u) {
vis[u] = 1;
for(int i = head[u] ; i != -1 ; i = edge[i].next) {
int v = edge[i].to;
if(!vis[v]) {
dfs(v);
dp[u] += min(edge[i].w,dp[v]);
}
}
}
int out[maxn];
int main() {
sd(n),sd(m),sd(s);
init();
rep(i,1,m) {
int u,v;
LL w;
sd(u),sd(v),slld(w);
addedge(u,v,w);
addedge(v,u,w);
out[u]++,out[v]++;
}
memset(vis,0,sizeof(vis));
rep(i,1,n) {
vis[i] = 0;
if(out[i] == 1) dp[i] = INF;
}
dp[s] = 0;
dfs(s);
plld(dp[s]);
return 0;
}
6、换根
d
p
dp
dp:
上面的树形
d
p
dp
dp,我们是从一个已经知道的根节点出发,对整棵树进行
d
p
dp
dp来得到答案,那如果我们需要对每个根节点进行一次树形
d
p
dp
dp,之后统计每一次得到的答案的最大值呢?难道我们就真的对每个节点进行一次树形
d
p
dp
dp吗?这样时间复杂度显然是
O
(
n
2
)
O(n^2)
O(n2)的,所以我们就可以用换根
d
p
dp
dp来做,这样可以把时间复杂度降到
O
(
n
)
O(n)
O(n)。具体怎么做呢?
1、任取一个节点作为根节点进行树形
d
p
dp
dp;
2、用树形
d
p
dp
dp得到的答案来更新每一个点作为根的答案。
所以我们就需要用到两次
d
f
s
dfs
dfs。
是不是还是很懵?我们先用一道例题来讲一下换根的思想。
题意:给你一颗树,让你找一个根节点,并且最小化
∑
i
=
1
n
d
e
p
[
i
]
\sum_{i=1}^{n}dep[i]
∑i=1ndep[i]。
思路:首先我们可以考虑任取一个节点作为根节点,比如1,并且用
d
p
[
]
dp[]
dp[]来记录以
i
i
i作为根节点时的答案。
那我们就可以用
d
f
s
dfs
dfs或者
b
f
s
bfs
bfs找出每个节点所在的深度,那么
d
p
[
1
]
=
∑
i
=
1
n
d
e
p
[
i
]
dp[1] = \sum_{i=1}^{n}dep[i]
dp[1]=∑i=1ndep[i]。之后我们就可以想如何通过
d
p
[
1
]
dp[1]
dp[1]来求解其他的值。
我们先来看这个图:
如果我们现在把2作为根,那是不是相当于把1及其子树的所有节点深度-1,2及其子树的所有节点深度+1,那不就相当于加减字数节点的个数嘛!!!所以我们还需要以节数组
s
u
m
[
]
sum[]
sum[]来记录每个节点及其子树的节点个数。
那么我们就有
d
p
[
v
]
=
d
p
[
u
]
−
s
u
m
[
v
]
+
(
n
−
s
u
m
[
v
]
)
dp[v] = dp[u] - sum[v] + (n - sum[v])
dp[v]=dp[u]−sum[v]+(n−sum[v])
PS.
1、
d
p
[
u
]
−
s
u
m
[
v
]
dp[u] - sum[v]
dp[u]−sum[v]相当于
u
u
u及其子树深度减一,
+
(
n
−
s
u
m
[
v
]
)
+(n-sum[v])
+(n−sum[v])相当于
u
u
u的节点深度加一;
2、记得开
l
o
n
g
l
o
n
g
long long
longlong。
AC代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <ctime>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define pii pair<int,int>
#define sd(x) scanf("%d",&x)
#define slld(x) scanf("%lld",&x)
#define pd(x) printf("%d\n",x)
#define plld(x) printf("%lld\n",x)
#define rep(i,a,b) for(int i = (a) ; i <= (b) ; i++)
#define per(i,a,b) for(int i = (a) ; i >= (b) ; i--)
#define mem(a) memset(a,0,sizeof(a))
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const LL INF = 1e18;
const LL mod = 1e9 + 7;
const int maxn = 1e6 + 7;
int head[maxn << 1];
struct Edge {
int to,next;
}edge[maxn << 1];
int tot;
void init() {
memset(head,-1,sizeof(head));
tot = 0;
}
void addedge(int u,int v) {
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
int n;
LL dp[maxn];
int dep[maxn];
int sum[maxn];
void dfs1(int u,int fa) {
for(int i = head[u] ; i != -1 ; i = edge[i].next) {
int v = edge[i].to;
if(v == fa) continue;
dep[v] = dep[u] + 1;
dfs1(v,u);
sum[u] += sum[v];
}
}
void dfs2(int u,int fa) {
for(int i = head[u] ; i != -1 ; i = edge[i].next) {
int v = edge[i].to;
if(v == fa) continue;
dp[v] = dp[u] + 1LL * n - 2LL * sum[v];
dfs2(v,u);
}
}
int main() {
sd(n);
init();
rep(i,1,n-1) {
int u,v;
sd(u),sd(v);
addedge(u,v);
addedge(v,u);
}
dep[1] = 0;
rep(i,1,n) sum[i] = 1;
dfs1(1,0);
dp[1] = 0;
rep(i,1,n) dp[1] += dep[i];
dfs2(1,0);
LL ans = INF;
rep(i,1,n) ans = min(ans,dp[i]);
plld(ans);
return 0;
}
那现在是不是大概明白了“换根”的思路了呢?
那来看道具体的换根
d
p
dp
dp的例题把。
题意:给你一颗树,边有边权。你需要找到一个点作为源点,并且最大化流量的最大值(类似于网络流的流法)。
思路:第一想法肯定就是以每个点作为源点进行树形
d
p
dp
dp,之后统计最大值。可这个数据范围是
1
e
5
1e5
1e5,
O
(
n
2
)
O(n^2)
O(n2)的算法铁定超时,所以我们就考虑换根
d
p
dp
dp。
看上面那个从题面上偷下来的图,我们按照上述步骤来:
1、我们先以1作为根进行树形
d
p
dp
dp,那如何进行树形
d
p
dp
dp呢?
我们用
d
p
[
u
]
dp[u]
dp[u]表示以
u
u
u为根节点的子树流量的最大值。
假设当前节点为
v
v
v,父节点为
u
u
u,那就有两种情况(在回溯的时候更新):
1)、
v
v
v为叶子节点,那么
d
[
u
]
+
=
d
[
v
]
d[u] += d[v]
d[u]+=d[v];
2)、
v
v
v不为叶子节点,那么
d
p
[
i
]
+
=
m
i
n
(
d
i
s
[
u
]
[
v
]
,
d
p
[
v
]
)
dp[i] += min(dis[u][v],dp[v])
dp[i]+=min(dis[u][v],dp[v])(因为边有流量的限制)。
之后我们考虑换根:
还是假设当前节点为
v
v
v,父节点为
u
u
u,还是分两种情况来看(不是在回溯的时候更新):
1)、
u
u
u的度数为1,那么
d
p
[
v
]
=
d
[
v
]
+
d
i
s
[
u
]
[
v
]
dp[v] = d[v] + dis[u][v]
dp[v]=d[v]+dis[u][v];
2)、
u
u
u的度数不为1,那么就有点复杂啦逃。
先举个实例来形象的描述一下啊。
还是上面那个图,假设现在
u
=
1
,
v
=
4
u = 1,v = 4
u=1,v=4,那我们现在已知的是1作为根节点的最大值(第一次
d
f
s
dfs
dfs是默认以1作为根节点)。
那么
d
[
1
]
−
m
i
n
(
d
[
4
]
,
13
)
d[1] - min(d[4],13)
d[1]−min(d[4],13)是不是就是节点1去掉节点
v
v
v为根的子树的贡献,那么
d
p
[
4
]
=
d
[
4
]
+
m
i
n
(
d
i
s
[
1
]
[
4
]
,
d
p
[
1
]
−
m
i
n
(
d
[
4
]
,
13
)
)
dp[4] = d[4] + min(dis[1][4],dp[1]-min(d[4],13))
dp[4]=d[4]+min(dis[1][4],dp[1]−min(d[4],13)),换成一般的符号表示就是
d
p
[
v
]
=
d
[
v
]
+
m
i
n
(
d
i
s
[
u
]
[
v
]
,
d
p
[
u
]
−
m
i
n
(
d
[
v
]
,
d
i
s
[
u
]
[
v
]
)
)
dp[v] = d[v] + min(dis[u][v],dp[u]-min(d[v],dis[u][v]))
dp[v]=d[v]+min(dis[u][v],dp[u]−min(d[v],dis[u][v]))
拿这一题就结束啦。
AC代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <ctime>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define pii pair<int,int>
#define sd(x) scanf("%d",&x)
#define slld(x) scanf("%lld",&x)
#define pd(x) printf("%d\n",x)
#define plld(x) printf("%lld\n",x)
#define rep(i,a,b) for(int i = (a) ; i <= (b) ; i++)
#define per(i,a,b) for(int i = (a) ; i >= (b) ; i--)
#define mem(a) memset(a,0,sizeof(a))
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const int INF = 1e9;
const LL mod = 1e9 + 7;
const int maxn = 2e5 + 7;
int head[maxn << 1];
struct Edge {
int to,next;
int w;
}edge[maxn];
int tot;
void init() {
memset(head,-1,sizeof(head));
tot = 0;
}
void addedge(int u,int v,int w) {
edge[tot].to = v;
edge[tot].w = w;
edge[tot].next = head[u];
head[u] = tot++;
}
int deg[maxn];
int d[maxn];
int dp[maxn];
void dfs1(int u,int fa) {
for(int i = head[u] ; i != -1 ; i = edge[i].next) {
int v = edge[i].to;
int w = edge[i].w;
if(v == fa) continue; ///去重
dfs1(v,u);
if(deg[v] == 1) d[u] += w;
else d[u] += min(d[v],w);
}
}
void dfs2(int u,int fa) {
for(int i = head[u] ; i != -1 ; i = edge[i].next) {
int v = edge[i].to;
int w = edge[i].w;
if(v == fa) continue;
if(deg[u] == 1) dp[v] = d[v] + w;
else dp[v] = d[v] + min(w,dp[u]-min(w,d[v]));
dfs2(v,u);
}
}
int main() {
int T;
sd(T);
while(T--) {
int n;
sd(n);
init();
mem(deg);
rep(i,1,n-1) {
int u,v,w;
sd(u),sd(v),sd(w);
deg[u]++,deg[v]++;
addedge(u,v,w);
addedge(v,u,w);
}
mem(d),mem(dp);
dfs1(1,0);
dp[1] = d[1];
dfs2(1,0);
int ans = 0;
rep(i,1,n) {
ans = max(ans,dp[i]);
}
pd(ans);
}
return 0;
}
d p dp dp例题
1、AtCoder ABC162 F - Select Half
题意:给你 n n n个数,让你选择 n / 2 n/2 n/2个两两不相邻的数,是他们的和最大。
思路:我们用 d p [ ] dp[] dp[]来表示符合题意的到第 i i i个位置的最大值。那我们接下来考虑状态转移:
- 当
i
i
i为奇数时:
(1) 如果选择第 i i i个数,那么 d p [ i ] = d p [ i − 2 ] + a [ i ] dp[i] = dp[i-2] + a[i] dp[i]=dp[i−2]+a[i].
(2) 如果不选择第 i i i个数,那么 d p [ i ] = d p [ i − 1 ] dp[i] = dp[i-1] dp[i]=dp[i−1]. - 当
i
i
i为偶数时:
(1)如果选择第 i i i个数,那么 d p [ i ] = d p [ i − 2 ] + a [ i ] dp[i] = dp[i-2] + a[i] dp[i]=dp[i−2]+a[i].
(2)如果不选择第 i i i个数,那么 d p [ i ] = s u m [ i − 1 ] dp[i] = sum[i-1] dp[i]=sum[i−1]
PS.
s
u
m
[
i
]
sum[i]
sum[i]表示前
i
i
i个奇数的前缀和。
可能这个2.2看起来有点迷糊,那我解释一下。
比如
i
=
4
i = 4
i=4,如果我们不选
a
[
4
]
a[4]
a[4]这个数,那我们就只能选
a
[
1
]
,
a
[
3
]
a[1],a[3]
a[1],a[3];
再比如
i
=
6
i = 6
i=6,如果我们不选
a
[
6
]
a[6]
a[6]这个数,那我们就只能选
a
[
1
]
,
a
[
3
]
,
a
[
5
]
a[1],a[3],a[5]
a[1],a[3],a[5];
。。。
这样讲明白了吧。
AC代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <ctime>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define pii pair<int,int>
#define sd(x) scanf("%d",&x)
#define slld(x) scanf("%lld",&x)
#define pd(x) printf("%d\n",x)
#define plld(x) printf("%lld\n",x)
#define rep(i,a,b) for(int i = (a) ; i <= (b) ; i++)
#define per(i,a,b) for(int i = (a) ; i >= (b) ; i--)
#define mem(a) memset(a,0,sizeof(a))
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const int INF = 1e9;
const LL mod = 1e9 + 7;
const int maxn = 2e5 + 7;
LL dp[maxn];
LL a[maxn];
LL sum[maxn];
int main() {
int n;
while(~sd(n)) {
rep(i,1,n) slld(a[i]);
sum[1] = a[1];
rep(i,1,n) {
if(i & 1) sum[i] = sum[i-1] + a[i];
else sum[i] = sum[i-1];
}
mem(dp);
rep(i,2,n) {
if(i & 1) dp[i] = max(dp[i-2] + a[i] , dp[i-1]);
else dp[i] = max(dp[i-2] + a[i] , sum[i-1]);
}
plld(dp[n]);
}
return 0;
}
2、nowcoder NC17065 - 子序列
题意:
小美有一个由
n
n
n个元素组成的序列{
a
1
,
a
2
,
a
3
,
.
.
.
,
a
n
a_1,a_2,a_3,...,a_n
a1,a2,a3,...,an},她想知道其中有多少个子序列{
a
p
1
,
a
p
2
,
.
.
.
,
a
p
m
a_{p_1},a_{p_2},...,a_{p_m}
ap1,ap2,...,apm}(
1
≤
m
≤
n
,
1
≤
p
1
<
p
2
,
.
.
.
,
<
p
m
≤
n
1 ≤ m ≤ n, 1 ≤ p_1 < p_2 ,..., < p_m ≤ n
1≤m≤n,1≤p1<p2,...,<pm≤n),满足对于所有的
i
,
j
(
1
≤
i
<
j
≤
m
)
,
a
p
i
p
j
<
a
p
j
p
i
i,j(1 ≤ i < j ≤ m), a_{p_i}^{p_j} < a_{p_j}^{p_i}
i,j(1≤i<j≤m),apipj<apjpi成立。
思路:
首先我们考虑到
i
,
j
i,j
i,j不独立,这样处理起来是很麻烦的,所以我们可以考虑将这个等式化简一下,怎么做呢?
为了方便,
a
i
,
a
j
a_i,a_j
ai,aj我用
i
,
j
i,j
i,j代替
a
i
j
<
a
j
i
⇒
j
×
l
o
g
(
a
i
)
<
i
×
l
o
g
(
a
j
)
⇒
l
o
g
(
a
i
)
i
<
l
o
g
(
a
j
)
j
a_i^{j} < a_j^{i}\Rightarrow j\times log(a_i) < i\times log(a_j) \Rightarrow \frac{log(a_i)}{i} < \frac{log(a_j)}{j}
aij<aji⇒j×log(ai)<i×log(aj)⇒ilog(ai)<jlog(aj)
这样我们题目就很好处理啦,我们可以考虑用
d
p
dp
dp来做。
我们用
d
p
[
i
]
dp[i]
dp[i]表示前
i
i
i个数中必须取第
i
i
i个数的方案数。
那么很显然,
d
p
[
i
]
=
∑
j
=
1
i
d
p
[
j
]
,
l
o
g
(
a
i
)
i
>
l
o
g
(
a
j
)
j
dp[i] = \sum_{j=1}^{i} dp[j], \frac{log(a_i)}{i} > \frac{log(a_j)}{j}
dp[i]=∑j=1idp[j],ilog(ai)>jlog(aj)
这个我们可以用
n
2
n^2
n2的算法来实现,答案很显然就是
∑
i
=
1
n
d
p
[
i
]
\sum_{i=1}^{n}dp[i]
∑i=1ndp[i].
AC代码如下:
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <cmath>
#include <ctime>
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define pii pair<int,int>
#define sd(x) scanf("%d",&x)
#define slld(x) scanf("%lld",&x)
#define pd(x) printf("%d\n",x)
#define plld(x) printf("%lld\n",x)
#define rep(i,a,b) for(int i = (a) ; i <= (b) ; i++)
#define per(i,a,b) for(int i = (a) ; i >= (b) ; i--)
#define mem(a) memset(a,0,sizeof(a))
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define fast_io ios::sync_with_stdio(false)
const int INF = 1e9 + 10;
const int mod = 1e9 + 7;
const int maxn = 1e2 + 7;
inline int read() {
char c = getchar();
int x = 0, f = 1;
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
x = x * 10 + (c - '0');
c = getchar();
}
return x * f;
}
int head[maxn << 1];
struct Edge {
int to, next;
}edge[maxn << 1];
int tot;
void init() {
memset(head, -1, sizeof(head));
tot = 0;
}
void addedge(int u, int v) {
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
LL qpow(LL a, LL b, LL p) {
LL res = 1;
while (b) {
if (b & 1) res = (res * a) % p;
a = (a * a) % p;
b >>= 1;
}
return res;
}
int a[maxn];
int dp[maxn];
bool Judge(int i, int j) {
return log(a[i]) / i > log(a[j]) / j;
}
int main() {
fast_io;
int n;
cin >> n;
rep(i, 1, n) {
cin >> a[i];
}
rep(i, 1, n) dp[i] = 1;
rep(i, 1, n) {
rep(j, 1, i) {
if (Judge(i, j)) {
dp[i] = (dp[i] + dp[j]) % mod;
}
}
}
int ans = 0;
rep(i, 1, n) {
//cout << dp[i] << " ";
ans = (ans + dp[i]) % mod;
}
cout << ans << endl;
return 0;
}