夏令营讲课内容整理 Day 5.

DP专场。。

动态规划是运筹学的一个分支, 求解决策过程最优化的数学方法。

我们一般把动态规划简称为DP(Dynamic Programming)

1.动态规划的背包问题

有一个容量为m的背包,有n个物品,每一个物品i的重量为w[i],价值为v[i]。

要求选择一些物品放入背包中,每种物品只能最多使用一次,使得在不超重的情况下让背包中所有物品价值总和最大。

正常向解法:设状态数组f[i][j]为把前i个物品放入一个容量为j的背包中所能获得的最大价值(以下同设),则状态转移方程为:

f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+v[i])

可优化至一维数组,令j从大到小枚举。

f[j] = max(f[j],f[j-w[i]]+v[i])

1.1完全背包

仍然是容量为m的背包,n个物品,每一个物品i的重量为w[i],价值为v[i]。

要求选择一些物品放入背包中,并且每一件物品可以选无限多次,使得在不超重的情况下让背包中所有物品价值总和最大。

f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i])

第一维也可以优化。

f[j] = max(f[j],f[j-k*w[i]]+k*v[i])

1.2多重背包

依旧是容量为m的背包,n个物品,每一个物品i的重量为w[i],价值为v[i]。

要求选择一些物品放入背包中,并且每一件物品的数量为s[i],使得在不超重的情况下让背包中所有物品价值总和最大。

它相对于完全背包的不同之处是每种物品可选的数量有一个上限。

f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]),k*v[i] ≤ j, k ≤ s[i]

它的时间复杂度是O(m*Sigma(s[i])),而且看起来并不能像之前那样优化了。。

实际上它是可以优化的。

用「二进制拆分」即可。

对每种物品,我们将它转化成若干个物品,其中每个物品有一个系数,这个物品的费用和价值均是原费用和原价值乘这个系数。令这些系数分别为1,2,4,...,2^k-1,s[i]-(2^k)+1,且k是满足s[i]-(2^k)+1>0的最大整数,例如,如果s[i] = 11,就将这种物品分成系数分别为1,2,4,4这四个物品。

这些物品的系数和能拼出[0,s[i]]中的任意一个整数。这样一来我们就把一个n个物品的多重背包问题转化成了一个Sigma(log s[i])个物品的01背包问题了。

时间复杂度为O(m*Sigma(log s[i]))

实际上,这还能再优化。。。

用单调队列。

单调队列形似一个普通队列,但其中的点按顺序存在某种单调性(我记得我Day1的整理上提到过)。

不同于优先队列,他俩内部结构不太一样。

注意转移式:f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]),这时候k的范围已经变成了k≤min(s[i],ceil(j/w[i])).

对于某一个i而言,因为j = k*w[i]+s的点只能转移到j = (k+1)*w[i]+s , (k+2)*w[i]+s...,所以我们可以按对w[i]取模的值对j进行分类。

把模w[i]相等的点放在一起进行转移,转移后的点会放入单调队列中。我们发现由于队列中的点j是从小到大的,所以会存在转移某一点k,队首点head已经不能作为转移点,即(k-head)/w[i]>s[i]。

显然,该点在以后的转移中也不会成为转移点,所以直接弹出队首即可,直到队首可以作为转移点。转移时选择队首的点作为转移点, 因为这个单调队列是保证转移式是单调递减的。

将当前点k压入队列时,需要判断队尾点tail与k的优劣。如果tail作为转移点不比k更优,显然tail在以后的转移也不会作为转移点了,弹出tail,直至tail比k更优。

对于每个i来说,单次转移是O(m)的,所以时间复杂度是O(nm).

2.动态规划的线性(序列)类(一维)问题

经典例题:一个长度为n的数列,第i个数为a[i],要求选择连续且非空的一段,使这一段中所有数的和加起来最大,输出这个最大和。

正在看这篇随笔的读者朋友,您应该会显然想到O(n^2)的做法。

我们考虑DP。令f[i]表示以i为结尾的最大和子段,那么转移只有:和前面的最大子段和连起来,或者自己单独成为一段。

f[i] = a[i] +max(f[i-1],0)

答案是max(f[i]),i∈[1,n]。

时间复杂度O(n)

2.1 LIS

有一个长度为n的序列,第i个数为a[i],求最长上升子序列的长度。

最长上升子序列的英文名叫Longest Increasing Subsequence,简称LIS。

正在看这篇随笔的读者朋友,您应该会显然想到O(n^2)的做法。

我们重点讨论O(nlogn)的做法。

我们设一个数组g[i],它表示长度为i的LIS中,作为结尾最小的数。

如果原数组a[i] = {6,7,1,5,4,3,4,2,8},那么g数组的变化应该是:

1:g [1] = 6

2:g [1] = 6; g [2] = 7

3:g [1] = 1; g [2] = 7

4:g [1] = 1; g [2] = 5

5:g [1] = 1; g [2] = 4

6:g [1] = 1; g [2] = 3

7:g [1] = 1; g [2] = 3; g [3] = 4

8:g [1] = 1; g [2] = 2; g [3] = 4

9:g [1] = 1; g [2] = 2; g [3] = 3; g [4] = 8

容易看出,每次g数组只会修改一个值或者加入一个值。而且无论如何,g数组总是保持单调递增。

显然,当LIS的长度相等时,结尾的数肯定是越小越好,这样才有更大的机会与后面的数相接,才能生成更长的LIS。这样一来,g数组一定是单调递增的,因为g数组不会存一个较长的LIS,使得它的结尾数比较短的LIS还小。每次考虑以i结尾的上升子序列时,我们只要在g数组中找到最大的的小于a[i]的位置j,令g[j+1] = len即可。

由于g单调,所以可以用二分查找来找到位置j,单次转移复杂度为O(logn)。

最终复杂度为O(nlogn).

例:有两个序列a,b,长度分别为n,m,求它们的LCS。

LCS指最长公共子序列。表示从两个序列中各自选出长度相等的子序列,这两个子序列的数对应相等。

设f[i][j]表示两个序列分别dp到第i,j个数,公共子序列的最大长度。

则有

f[i][j] = max(f[i-1][j],f[i][j-1]) (a[i]!=b[j])

f[i][j] = f[i-1][j-1] + 1(a[i] == b[j])

时间复杂度O(n^2)。

例2:在上例条件不变的情况下,求LCIS

LCIS指最长公共上升子序列。表示从两个序列中各自选出长度相等的上升子序列,这两个子序列的数对应相等。

设f[i][j]表示两序列分别以i,j结尾的LCIS。

一个简单的想法是

f[i][j] = max(f[k][l]+1) (k < i , l < j, a[k] < a[i], b[l] < b[j], a[i]==b[j])

我们先枚举i,再枚举j,当a[i] != b[j] 时f[i][j] = 0。但当a[i] >b[j] 时,在同一个i下,所有f[k][j] (k<i)是可以作为转移点转移后面的状态f[i][j‘],因为存在方案的f[k][j]一定保证a[k]<a[i],b[j]<b[j‘]。

我们可以开一个数组来记录第二维为j且第一维小于i的f的最大值,不过第一维是可以被优化掉的。

设计状态f[i]表示b序列以b[i]结尾的LCIS,具体实现:

1 for (int i=1;i<=n;i++){
2     int k= 0;
3     for (int j=1;j<=m;j++)
4         if (a[i] == b[j])
5             f[j] = max(f[j],k+1);
6         else
7         if (a[i] > b[j])
8             k = max(k,f[j]);
9 }

空间复杂度降为O(m)。

3.树形(树上)DP

顾名思义,是在树上所做的动态规划,其基础是树具有严格的层数关系而不会重复的特性。

一般来说树形DP是处理子树的信息以及其相互关系来进行转移。其状态一般会表示成以i为根的子树的DP值。

3.1邻接表存树

在之前的知识点整理上提前说了,这里只提供代码。

 1 struct Edge_tree{
 2     int u,v,w;
 3     int next;
 4
 5 };
 6 Edge_tree edge[maxn];
 7 int cnt = 0;
 8 int first[maxn];
 9 void add_edge(int from,int to,int dis){
10     edge[++cnt].u = from;
11     edge[cnt].v = to;
12     edge[cnt].w = dis;
13     edge[cnt].next = fisrt[from];
14     first[from] =cnt;
15
16     edge[++cnt].v = from;
17     edge[cnt].u = to;
18     edge[cnt].w = dis;
19     edge[cnt].next = first[to];
20     first[to] = cnt;
21
22 }
23
24
25 void dfs_tree(int x,int fa){
26     //cout << x << " ";
27     for (int i = first[x];i!=0;i = edge[i].next)
28         if (edge[i].v != fa)
29             dfs_tree(edge[i].v,x);
30 }

3.2最大连通子树

有一个n个节点的树,每个点有点权a[i],求一棵连通子树使点权之和最大。

(n ≤ 10^5,|a[i]| ≤ 10^9)

f[u] = a[u] + Sigma(max(f[v],0))

最终答案是max(f[i]), i∈[1,n]

3.3树的直径

有一个n个节点的树,每条边有边权w[i],求一条路径使得它的所有边权和最大。

这条路径就叫做这棵树的直径。

(n ≤ 10^5,|a[i]| ≤ 10^9)

如果边权保证非负,我们可以用两遍bfs求得直径。具体做法是第一次随便选一个点,bfs求得与它距离最远的点x,再从x出发bfs求得与x距离最远的点y,x与y之间的距离就是树的直径。

若边权存在负数,则不能使用这个方法。

设f[u]表示在u为根的子树中,存在点u的最大路径边权和(u可以为端点也可以为中间的点)

设g[u]表示在u为根的子树中,存在点u的最大路径边权和(u只能为端点,即由u发出的一条链)

f[u] = max( firstmax{g[v] + w(u,v)}, 0) + max(secondmax{g[v] + w(u,v)},0)

g[u] = max{g[v] + w (u,v)}

4.区间DP以及其他DP

4.1区间DP

顾名思义,是在一个区间上进行的一系列动态规划,一般考虑对于每段区间,它们的最优值都是由两段或者更多段的小区间点的最优值得到,是分治思想的一种应用。

一般定义状态f[i][j]表示从区间i到j的DP最优值,转移时枚举中间点k,从f[i][k],f[k+1][j]来进行合并

例题:合并石子

n堆石子排成一列,每堆石子有一个重量w[i],每次可以合并相邻的两堆石子,一次合并的代价为二者重量之和。问怎样安排合并顺序使得代价最小。

n,w ≤100

解:用s[i]表示石子的前缀和,有

f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[i-1])

时间复杂度O(n^3)。

变式:n堆石子排成一个圆,其他条件不变。

解:在后面加上一条排列相同的石子堆,扩展到2n-1个石子。依然是用s[i]表示w[i]的前缀和。有

f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[i-1])

时间复杂度O(n^3),最终答案是min(f[i][i+n-1]),其中1 ≤ i ≤ n。

变式:使最终代价最大, 其他条件不变

解:贪心的来想,我们肯定是让每个石子重量都尽可能多的被计算,也就是说每次只合并一个石子进来应该是一个最优的策略。

f[i][j] = max(f[i+1][j],f[i][j-1])+s[j]-s[i-1]

时间复杂度O(n^2)。

4.2棋盘DP

非常好想的一类DP,在一个二维网格(地图)上做DP。

一般设f[i][j]表示走到(i,j)位置上的最优值。

4.3DAG上的DP

给定一个DAG(有向无环图),要求统计一些信息。

DAG是一个比较规则的结构,我们可以对这些点进行拓扑排序后再DP。

4.4状压DP

(GTMDNOIP2016D2T3

状态压缩DP,通过二进制位上的0/1表示状态,一般用于要记录一段较小规模的状态且该状态包含信息较多的问题。

就目前来说,在NOIP史上只有去年考到了。

谁知道今年还会考什么奇怪的东西呢……

4.5概率期望DP

(GTMDNOIP2016D1T3

这种题目会丧心病狂的让你求某一事件的期望或者概率。。

并不想写这类丧心病狂的东西。

4.6数位DP

(这已经超纲了吧。

这种题目一般会让你统计某一区间内的与数位或与数相关的信息,但由于区间较大而没法暴力求解,所以要在数位上进行DP。

就目前来说,没考过,谁知道今年考不考。。

5.相关优化

5.1前缀和优化

一般用于转移点的取值是一段连续的区间,做一下前缀和可以把转移从O(n)降至O(1)。

5.2滚动数组优化

这个还挺常见的。一般用于二维及以上的DP。如果某一维i的dp值只与i-1的dp值有关那么我们不用存这一维全部的情况,用0/1状态来存储当前状态和转移点状态就可以了,这样会降低空间复杂度。

5.3单调性优化

利用对某一属性的单调性来加速转移,体现在减少转移点数量和快速查询转移点的情况,常见的是单调队列, 二分查找等。

5.4数据结构优化

即利用一些数据结构来进行优化。NOIP阶段常用堆。

5.5其他优化

减少冗余状态

利用数据结构的特殊性质

女装

时间: 2024-10-13 23:54:16

夏令营讲课内容整理 Day 5.的相关文章

夏令营讲课内容整理Day 0.

今年没有发纸质讲义是最气的.还好我留了点课件. 第一次用这个估计也不怎么会用,但尝试一下新事物总是好的. 前四天gty哥哥讲的内容和去年差不多,后三天zhn大佬讲的内容有点难,努力去理解吧. 毕竟知识还是需要消化的. 这里我只整理知识点,每天上午评测的题目我会单独处理. 嗯大概就是这样了. 写完后我就会考虑发到博客园里.

夏令营讲课内容整理 Day 3.

本日主要内容是树与图. 1.树 树的性质 树的遍历 树的LCA 树上前缀和 树的基本性质: 对于一棵有n个节点的树,必定有n-1条边.任意两个点之间的路径是唯一确定的. 回到题目上,如果题目读入的是树上所有的边,则我们应该想到: 每个点的父亲是谁 每个点的深度 每个点距离根节点的距离 其他的附加信息(例如:子树和,子树最大值..) 遍历整个树的代码如下: 1 void dfs(int now) 2 { 3 deep[now]=deep[fa[now]]+1; 4 sum[now]=value[n

夏令营讲课内容整理 Day 4.

本日主要内容就是搜索(打暴力 搜索可以说是OIer必会的算法,同时也是OI系列赛事常考的算法之一. 有很多的题目都可以通过暴力搜索拿到部分分,而在暴力搜索的基础上再加一些剪枝优化, 就有可能会拿到更多的分数. 有句话说的好嘛,骗分过样例,暴力出奇迹. 真的可以出奇迹的,只要你用得好. 1.搜索的概念 在一个给定的空间内,运用一定的查找(遍历)方式,直到找到目标解(状态)的过程,我们称之为搜索. 搜素是尝试性的,搜索是无脑的,搜索是朴素的,搜索在很多时候是显然的,搜索应该总是暴力的.但搜索也是很常

夏令营讲课内容整理Day 1.

主要内容是栈和队列. 1.  栈 运算受到限制的线性表.只允许从一端进行插入和删除等操作.这一端便是栈顶,另一端便是栈底. 其实可以把栈想象层任何有底无盖的柱状的容器...毕竟栈满足后进先出的特性.计算机当中调用函数时,中间结果便会保存到「系统栈」中.递归过程也需要栈的协助 . 实现:STL or 手写(请参照一本通 or 课件) 一般操作:判断栈空/满.入栈.出栈,判断栈的大小(请参照一本通 or 课件) 1.1 单调栈 顾名思义,保证内部元素单调(单增或单减)的栈.我们只要在插入新元素的时候

夏令营讲课内容整理 Day 6 Part 1.

Day6讲了三个大部分的内容. 1.STL 2.初等数论 3.倍增 Part1主要与STL有关. 1.概述 STL的英文全名叫Standard Template Library,翻译成中文就叫标准模板库. 它有点类似于一个大型的工具箱,里面包含许多实用工具,可以拿过来直接用而大部分情况下无需去深入探究其内部原理. 不知道从什么时候开始,CCF不再限制选手使用STL,所以在OI赛事中STL被广泛应用. 它分为六个大部分: 1)容器 containers 2)迭代器 iterators 3)空间配置

夏令营讲课内容整理 Day 6 Part 2.

Day 6的第二部分,数论 数论是纯粹数学的分支之一,主要研究整数的性质 1.一些符号: a mod b 代表a除以b得到的余数 a|b a是b的约数 floor(x) 代表x的下取整,即小于等于x的最大整数,也可以认为是直接舍去小数部分 (这个应该是一个符号,但我不知道怎么打出来..下面那个ceil也是) ceil(x) 代表x的上取整,即大于等于x的最小整数,也可以认为是直接舍去小数部分再+1. gcd(a,b) 表示a与b的最大公约数 lcm(a,b) 表示a与b的最小公倍数 累加符号∑

夏令营讲课内容整理 Day 2.

本日主要内容是并查集和堆. 并查集 并查集是一种树型的数据结构,通常用来处理不同集合间的元素之间的合并与查找问题.一个并查集支持三个基本功能:合并.查找和判断.举一个通俗的例子,我和lhz认识,lhz和hzc认识,那么也就可以断定我和hzc认识. 依照并查集的思想,我们把所有要待处理的元素a1,a2,a3....an这n个元素都看作是一个单独的集合,初始状态每个集合都只有一个元素.我们就可以把并查集的合并操作理解为集合之间的取并集操作. 作为一个树形结构,在一个由许多这样的集合构成的森林中,每个

Google C++ 风格指南内容整理

之前一直没有全面的看过Google C++风格指南,现在很多公司进行C++开发都要求按照Google C++风格.在这个网站 http://zh-google-styleguide.readthedocs.org/en/latest/contents/  有人已经把其翻译成中文.为了便于以后查看,下面的内容完全是来自于这个网站,只是把多个网页的内容整理放在了一起. 1.      头文件: 通常每一个.cc文件都有一个对应的.h文件.也有一些常见例外,如单元测试代码和只包含main()函数的.c

网页格式化排版代码,专用信息采集后的内容整理

public static string ClearHtml(string content) { Regex regex = new Regex(""); //首先把p标签的属性去掉,只留<p> regex = new Regex(@"<p.*?>", RegexOptions.IgnoreCase | RegexOptions.Singleline); content = regex.Replace(content, "<p