动态规划
前言
本篇依据y总的方法,采用集合的框架力来力求解释好dp的原因。
框架的整体是:
手把手如何写动态规划
- 第一点肯定是经验了,写了一定量的题目后你才能动态规划知其所以然。
- 难点是动态规划的计算上面难点就是状态的计算上面了。动态规划是一种特殊的最短路问题(在拓补图上的)。从图论方面考虑状态的计算,其实就是,不同状态之间的转换关系(有向边的关系)
转换关系一般有两大类:
- 用其所依赖的状态来更新当前状态。(80%)
- 用当前状态更新依赖它的状态。
画图来说就是
这两种其实是等价的。
下面以两个题目来进行说明
最长路径
首先我们可以写出动态规划的表达式(很大程度考经验),dp[i]表示 y以节点i结尾的最长路径长度。那么我们如何进行更新呢,首先这个点的前一个状态是什么情况我呢吧不好知道,但是因为由这个点指向的下一个点的情况我们是可以很好的知道的,就是该点的路径长度+1,(就是上图第二类的情况),我们可以有当前状态更新依赖它的状态。
那么更新的顺序是什么呢?结合题目的有向无环图,那么我们就可以按照拓扑序的顺秀来进行更新。
代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 2e5 + 10;
int n, m, cnt;
int h[N], ne[N], e[N], in[N];
int f[N];
void add(int u, int v) {
e[++cnt] = v, ne[cnt] = h[u], h[u] = cnt;
}
void tosort() {
queue<int>q;
for (int i = 1; i <= n; ++i)
if (!in[i])
q.push(i);
while (q.size()) {
int u = q.front();
q.pop();
for (int i = h[u] ; ~i ; i = ne[i]) {
int v = e[i];
f[v] = max(f[v], f[u] + 1);
if (--in[v] == 0)
q.push(v);
}
}
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
while (m--) {
int a, b;
cin >> a >> b;
add(a, b);
in[b]++;
}
tosort();
int res = 0;
for (int i = 1 ; i <= n ; ++i)
res = max(res, f[i]);
cout << res << endl;
return 0;
}
网格
题目转送门
上面一题是第二类情况,那么这题就讲一下第一类的情况;
首先我们定义的动态规划的状态是:dp[i][j] : 表示从(1,1)到(i,j)的路径数量。
由题意,对于每一个dp[i][j] ,他都是只能从上方或者左方过来的,因此对于每一个dp[i][j]加上它的这两个方向的路径数量即可。
代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010, mod = 1e9 + 7 ;
int n, m ;
char g[N][N];
int f[N][N];
int dx[2] = {-1, 0}, dy[2] = {0, -1};
int main() {
cin >> n >> m;
for (int i = 0 ; i < n ; ++i)
cin >> g[i];
f[0][0] = 1;
for (int i = 0 ; i < n ; ++i)
for (int j = 0 ; j < m ; ++j) {
if (g[i][j] == '#')
continue;
for (int k = 0 ; k < 2 ; ++k) {
int xx = i + dx[k], yy = j + dy[k];
if (xx < 0 || xx >= n || yy < 0 || yy >= m)
continue;
if (g[xx ][yy ] == '.')
f[i][j] = (f[i][j] + f[xx][yy]) % mod;
}
}
cout << f[n - 1][m - 1] << endl;
return 0;
}
最长上升子序列问题
求最长上升子序列
首先我们依据框架来写一下思路
我们讲一下状态计算中为什么划分成这样。
f[i]
是以A[I]
为结尾的所有情况中的最大值。那么我们还是引入last这个概念,对于所有以A[i]
结尾的上升序列,由于最后一个都是A[i]
那么它们的不同点就是去掉A[I]
之后的第一个数,即倒数第二个数。因此我们都去掉A[i]
而去枚举前面满足,而有含义可以知道这些就是f[1~(i-1]
的定义。因此我们就枚举从1~(i-1)满足上升的。取最大值。
核心代码
for(int i = 2 ; i <= n ;++i){
for(int j = 1 ; j < i ; ++j )
if(a[i] > a[j]) f[i] = max(f[i] , f[j] + 1);
}
上面做法很明显是 O ( n 2 ) O(n^2) O(n2)的一个做法。很明显不能接收。那么我们看如何进行优化呢?重复计算主要是在第二层循环上边,因为对于每一个i我们都要去前边找一个最优的前缀序列,不过呢这个题不能想下边最长公共子序列那样记录一个最值,因为在这里后边的值是可能比前边小的。因此我们利用贪心的思想,对于同一长度的的LIS我们肯定是希望结尾越小越好。同时这样维护的序列由于是具有单调性的,因此我们就可以利用二分来优化最终使得算法时间复杂度为: O ( n l o n g n ) O(nlongn) O(nlongn)
f[i]
的定义是:表示长度为i的LIS结尾元素的最小值。到时候我们的答案就是i的值。- 为什么是对的?对于一个LIS序列当遍历到后边的时候如果如果我们更新了结尾的值,由贪心思想这是最优的,同时也是可以的,对于如果我们用后边一个值去修改一个在中间的值,表面上我们不可以这样因为我们要按顺序来取,但是这不会影响最终的结果,因为它不会增加LIS的长度。具体来说,我们想修改的值只是结尾的值,然其最小化,不过如果加上特判只修改结尾值会麻烦,而都修改不会影响最终结果,那何乐而不为之呢?因此我们用这个方法能求出正确的LIS的长度,但里边存的值不一定是正确的LIS序列。即
但是!!!B[ ] 中的序列并不一定是正确的最长上升子序列 - 我们二分找的是比a[i]大于或等于的第一个位置,因此我们可以利用
lower_bound( )函数:
来实现。//返回第一个大于等于val值的位置
核心代码:
#include <stdio.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010, INF = 0x3f3f3f3f;
int n, cnt, a[N];
int f[N];
int find(int num) {
// 由于加1之后,那么减完 f之后就相当于f以1开始存的下标
return lower_bound(f + 1, f + 1 + cnt, num) - f;
}
int main() {
cin >> n;
for (int i = 1 ; i <= n ; ++i)
cin >> a[i], f[i] = INF;
f[1] = a[1], cnt = 1;
//从第2个数开始
for (int i = 2 ; i <= n ; ++i) {
if (a[i] > f[cnt]) // 大于的话直接接在后边
f[++cnt] = a[i];
else
f[find(a[i])] = a[i];
}
cout << cnt << endl;
return 0;
}
上面做法就有点遍历dp了,那么我们对dp式子进行分析一下:
我们再来回顾O(n^2)DP的状态转移方程:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
我们在递推F数组的时候,每次都要把F数组扫一遍求F[ j ]的最大值,时间开销比较大。我们可以借助数据结构来优化这个过程。那么我们如何优化呢?我们耗时是在一个一个的去与前边满足条件的取最大值,那么我们是否可以更快的进行呢?我们就可以采用树状数组来进行维护,采用以数值范围建树的思想,将LIS长度的最大值记录下来。那么取最大值这个过程优化了,不过我们利用树状数组维护的时候是与该编号前边的所有取一个最大值,因此对于该编号我们要保证它的值必须要是大的(上升),因此我们就可以用结构体来存值和编号,对值进行排序,然后从前往后遍历,那么我们在遍历的时候就一定是满足上升的性质。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn =103,INF=0x7f7f7f7f;
struct Node{
int val,num;
}z[maxn];
int T[maxn];
int n;
bool cmp(Node a,Node b)
{
return a.val==b.val?a.num<b.num:a.val<b.val;
}
void modify(int x,int y)//把val[x]替换为val[x]和y中较大的数
{
for(;x<=n;x+=x&(-x)) T[x]=max(T[x],y);
}
int query(int x)//返回val[1]~val[x]中的最大值
{
int res=-INF;
for(;x;x-=x&(-x)) res=max(res,T[x]);
return res;
}
int main()
{
int ans=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&z[i].val);
//如果题目数据没有说各不相同的话,需要去重。
//如果不去重那就是求最长非严格单调上升子序列的长度
z[i].num=i;//记住val[i]的编号,有点类似于离散化的处理,但没有去重
}
sort(z+1,z+n+1,cmp);//以权值为第一关键字从小到大排序
for(int i=1;i<=n;i++)//按权值从小到大枚举
{
int maxx=query(z[i].num);//查询编号小于等于num[i]的LIS最大长度
modify(z[i].num,++maxx);//把长度+1,再去更新前面的LIS长度
ans=max(ans,maxx);//更新答案
}
printf("%d\n",ans);
return 0;
}
最长非严格递增子序列
这个和上面是几乎一样的,由于是非严格因此可以等于,因此我们就有可能是等于的情况而已
核心代码:
for(int i = 2 ; i <= n ;++i){
for(int j = 1 ; j < i ; ++j )
if(a[i] >= a[j]) f[i] = max(f[i] , f[j] + 1);
}
对于第二种方法:
for (int i = 2 ; i <= n ; ++i) {
if (a[i] >= f[cnt]) // 大于的话直接接在后边
f[++cnt] = a[i];
else
f[find(a[i])] = a[i];
}
至少修改多少次能将序列变为上升序列
- 对于非严格递增序列,我们只需要用序列的总长度减去最长非严格递增序列的长度即可。因为对于不在最长非严格递增序列的数字我们可以将其改为与临近的数字相等就可以实现了。
- 对于用严格递增序列,我们先构造出一个数组b,
b[i] = a[i] - 1
,然后求b数组的最长非严格递增子序列的长度,然后用序列总长度减去就得到正确答案了。原因是:对于严格递增序列我们有 a [ i ] > a [ j ] ( i > j ) a[i] > a[j] (i > j ) a[i]>a[j](i>j)等价变形为 a [ i ] − i > = a [ j ] − j a[i] - i >= a[j] - j a[i]−i>=a[j]−j.对于这个形式我们发现出现了等于号,因此我们就按上述方法构造出一个新的序列。然后用这个新的序列来求。
最长公共上升子序列
题目转送门
下面就来重点讲一下,状态计算的部分。
首先 ,依据最长公共子序列的思想,因为b[j]一定在这个集合中了那么我们就看a[i]这个元素,由题目有两个条件,一个是公共,这个是两个序列之间的关系,一个是上升是序列内部的关系。我们就由公共,分为a[i]不在公共上升子序列中和a[i]在公共上升子序列中两部分。对于前一个部分,
也就是f[i-1][j]
的含义了,我们都是从含义出发来找的。对于后一部分,我们需要借助,lat这个概念,这个是什么意思呢,就最后一个不同的地方,由于后半部分a[i] == b[j]的,那么其不同点就是b[j]上的一个元素是什么了。而b[j]元素前边有的元素个数可能情况是0~j-1 个。对于0的情况,因为b[j]前边没有元素,因此只要我满足公共就一定是上升了也就是1.(因为就自己一个肯定是上升的)。对于 1 ~ j-1 。我们只要它们的值小于 b[j] 那么就可以进行去max的计算,而在取max的时候,我们的式子是f[i][j] = max(f[i][j] , f[i-1][k] + 1);
首先解释下i - 1
,和为什么我们以有无a[i]来区分,这与我们的遍历有关,我们外层循环是 a ,如果我们外层循环是 b,那么我们也可以按理将b[i]是否在来进行划分。然后是f[i-1][k] + 1
。 因为在这个的前提是a[i] = a[j] ,那么就已经满足了公共的条件了,又有b[k] < b[j] ,那么又满足了上升的条件那么,f[i-1][k]
是这个的前一部分位置,1
是a[i] == b[j]
这个部分。
我们有以下核心代码:
for(int i = 1 ; i <= n ; ++i){
for(int j = 1 ; j <= n ; ++j){
f[i][j] = f[i-1][j];
if(a[i] == b[j]){
int maxv = 1;
for(int k = 1 ; k < j ; ++k)
if(b[k] < b[j])maxv = max(maxv , f[i-1][k] + 1);
f[i][j] = max(maxv , f[i][j]);
}
}
}
int res = 0 ;
for(int i = 1 ; i <= n ; ++i)res = max(res , f[n][i]);
这个算法是
O
(
n
3
)
O(n^3)
O(n3)的一个数量级,我们就想办法对其进行优化一下。
由于a[i] == b[j]
,上式代码就变为了:if(b[k] < a[i])maxv = max(maxv , f[i-1][k] + 1);
那么第三层循环求的就是:
这里边小于 a[i]
的 , 因为这个是固定的,因为b里边哪些值小于a[i]都是已经成定居了的。所以这个循环就是求所有的b,b满足小于a[i]对应的方案数+1的一个最大值。
for(int i = 1 ; i <= n ; ++i){
int maxv = 1;
for(int j = 1 ; j <= n ; ++j){
f[i][j] = f[i-1][j];
if(a[i] == b[j])f[i][j] = max(maxv , f[i][j]);
if(b[j] < a[i]) maxv = max(maxv,f[i-1][j] + 1 );
}
}
分级问题
有了前面的铺垫我们就直接来说如何进行转态计算的。首先对于
f[i][j]
这个状态它是定的了,就是我第i个位置放的一定是 b[j]这个数了,那么在这个情况下(就是指第i个位置放的一定是 b[j]这个数的所有的可能情况下) ,还是引用last这个概念,我们的第一个不同点应该是上一个位置所放的值,由于要符合非严格单调递增的性质,我们上一个位置能放的值的可能就是1~j
因此我们就对该问题进行了划分。至于如何求呢我们看下边
我们发现如果我们用循环来计算,就会有很多的重复计算。我们发现对于后一个j来说除了f[i-1][j+1] 是与j的时候不同的其余都是一样的。因此我们就可以用一个值来维护这个最小值。
- 因为这题是有非严格递增和递减两种情况,因此我们这两种情况中取一个最小值即可。那么如果我们先求了递增,那么只需要,将原数组翻转一下即可。
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 2010 , INF = 0X3f3f3f3f3f;
int n , a[N] , b[N];
int f[N][N];
int dp(){
for(int i = 1 ; i <= n ; ++i)b[i] = a[i];
sort(b+1,b+1+n);
for(int i = 1 ; i <= n ; ++i){
int minv = INF;
for(int j = 1 ; j <= n ; ++j){
minv = min(minv , f[i-1][j]);
f[i][j] = minv + abs(b[j] - a[i]);
}
}
int res = INF;
for(int i = 1 ; i <= n ;++i)res = min(res , f[n][i]);
return res;
}
int main(){
scanf("%d",&n);
for(int i = 1 ; i <= n ; ++i)scanf("%d",&a[i]);
int res = dp();
reverse(a+1 , a+1+n );
res = min(res , dp());
printf("%d\n",res);
return 0;
}
移动服务
解题思路:
首先我们要想如何表示状态呢?我们可以用完成了第几个请求作为一个阶段,但是光有这个信息还是不够的,我们就需要附加一些信息。在这个题目中,我们用三个服务员的位置来作为附加的信息。因此我们就有了,
f[i][x][y][z]
表示的含义是处理第i个请求后三个服务员所在的位置分别为x,y,z。不够如果我们直接这样计算时间上直接会TM的。那么我们就需要分析这些附加的信息是否有关联性呢?由题目含义出发,当我们处理了第i个请求后肯定有一个服务员是在pos[i]这个位置上的,因此后边的三个信息我们只需要两个即可。到了这里我们先画出初步的框架出来。
那么下一个难点就是状态的计算上面了。动态规划是一种特殊的最短路问题(在拓补图上的)。从图论方面考虑状态的计算,其实就是,不同状态之间的转换关系(有向边的关系)
转换关系一般有两大类:
- 用其所依赖的状态来更新当前状态。(80%)
- 用当前状态更新依赖它的状态。
画图来说就是
这两种其实是等价的。
那么我们会过头来看一下这个问题,这个状态的状态中,对于当前状态
f[i][x][y]
它出去的状态就只有三种,无非是x服务员去,要么是y去,要么是z去。而对于它的入边就很复杂。因此这一题我们就选用第二种来进行计算。
启发:
- 求线性DP的问题,一般先确定“阶段”。如果“阶段”不足以表示一个状态,则需要把所需的附加信息也作为状态的维度。
- 在确定DP的状态的时候,要选择最小的能够覆盖整个状态空间的:维度集合。好比这题我们当然可以用四维的来表示,但是如果能用三维来表示不是更好。
代码:
#include<stdio.h>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010 , M = 210;
int n , m , pos[N];
int w[M][M] , f[N][M][M];
int main(){
scanf("%d%d",&m,&n);
for(int i = 1 ; i <= m ; ++i)
for(int j = 1 ; j <= m ; ++j)
scanf("%d",&w[i][j]);
for(int i = 1 ; i <= n ;++i)scanf("%d",&pos[i]);
memset(f , 0x3f , sizeof f);
pos[0] = 3 , f[0][1][2] = 0;
for(int i = 0 ; i < n ; ++i)
for(int x = 1 ; x <= m ; ++x)
for(int y = 1 ; y <= m ; ++y){
int z = pos[i] , u = pos[i+1] , val = f[i][x][y];
if(x == y || x == z || y == z)continue;
f[i+1][x][y] = min(f[i+1][x][y] , val + w[z][u]);
f[i+1][z][y] = min(f[i+1][z][y] , val + w[x][u]);
f[i+1][x][z] = min(f[i+1][x][z] , val +w[y][u]);
}
int res = 0x3f3f3f3f;
for(int x = 1 ; x <= m ; ++x)
for(int y = 1 ; y <= m ; ++y){
int z = pos[n]; //因为是顺序处理请求,因此最后处理的应该是这个请求。
if(x == y || y == z || x == z)continue;
res = min(res , f[n][x][y]);//
}
printf("%d\n",res);
return 0;
}
传纸条
解题思路:
这题首先我们回想一下如果是只能走一条路径的时候我们是如何定于状态的:
f[i][j]
:表示走到 ( x i , y j ) (x_i,y_j) (xi,yj)这个路径的和的最大值。那么我们可以类似的定义为:f[x1][y1][x2[y2]
。不过由上面的启发我们试着想想能不能缩小一下状态空间呢?我们用了两个坐标,是为了判断是否位于同一个格子上即x1 == x2 && y1 == y2
,不过这样分太细了。如果我们记录的是横纵坐标的和,那么如果和相同的情况下,只有x相同那么就可以判断出来了。因此我们最终的状态表示是:f[k]x1][x2];
那么什么如何由划分的子区间的含义如何得到表达式呢?
类比,我们得到四个区间的表达式分别为:
- 两个人同时向右走,最大分值是 f[k - 1, i, j] + score(k, i, j);
- 第一个人向右走,第二个人向下走,最大分值是 f[k - 1, i, j - 1] + score(k, i, j);
- 第一个人向下走,第二个人向右走,最大分值是 f[k - 1, i - 1, j] + score(k, i, j);
- 两个人同时向下走,最大分值是 f[k - 1, i - 1, j - 1] + score(k, i, j);
还有一个注意点是x的范围,我们有 1 < = x < = n 和 1 < = k − x < = m 1 <= x <= n 和 1 <= k - x <= m 1<=x<=n和1<=k−x<=m 得到
m a x ( 1 , k − m ) < = x < = m i n ( n , k − 1 ) max(1,k-m) <= x <= min(n,k-1) max(1,k−m)<=x<=min(n,k−1)
对于本题还可以与方格取数有关,相关的证明为这篇博客:博客
代码:
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 55;
int n , m ;
int w[N][N], f[N<<1][N][N];
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ; ++i)
for(int j = 1 ; j <= m ; ++j)
cin>>w[i][j];
for(int k = 2 ; k <= n + m ; ++k)
for(int x1 = max(1 , k - m) ; x1 <= min(n , k -1) ; ++x1)
for(int x2 = max(1,k-m); x2 <= min(n, k - 1) ; ++x2)
{
int t = w[x1][k-x1] ;
if(x1!=x2) t += w[x2][k-x2];
for(int a = 0 ; a <= 1 ; ++a)
for(int b= 0 ; b <= 1 ; ++b)
f[k][x1][x2] = max(f[k][x1][x2] , f[k-1][x1-a][x2-b] + t);
}
cout<<f[n+m][n][n]<<endl;
return 0;
}
排序不等式
背包问题
前言
相信大家应该都看过很多讲解背包问题的博客或者视频了。不过这里采用的还是利用上面的框架来进行分析。希望都大家理解上面和运用上面有一个更深的理解。同时在背包问题中我的顺序上也是做了一些巧妙的安排。
01背包问题
如下图:我们主要来讲一下状态计算这部分。对于
f[i][j]
我们对比一下这些在这个集合中的所有方案的上一个不同点,对于i来说有的可能是没有去取第i个物品体积就是j了。因此先分为取和不取两个划分。对于取的这部分,那么它们最后一个都是取的i物品是一样的,那么它们的last不同点就是没取第i个物品的j的不同。
我们一般是分为变和不变两个部分,这些方案最后都是取了V[I]这个价值是一样的,那么不一样的就是前 i - 1 ,个物品时候的总体积。而我们为了能取到那么总体积就不能小于W[I[
小技巧:
- 对于一个小数:可以用
memset(f , 0xcf , sizeof f);
来初始化。
代码:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1010;
int n , V;
int v[N] , w[N];
int f[N][N];
int main(){
memset(f , 0xcf , sizeof f);
cin>>n>>V;
for(int i = 1 ; i <= n ; ++i)cin>>w[i]>>v[i] ;
f[0][0] = 0;
for(int i = 1 ; i <= n ; ++i)
for(int j = 0 ; j <= V ; ++j){
f[i][j] = f[i-1][j];
if(j >= w[i])
f[i][j] = max(f[i][j] , f[i-1][j-w[i]] + v[i]);
}
int res = 0;
for(int i = 1 ; i <= V ; i++) res = max(res , f[n][i]);
cout<<res<<endl;
return 0;
}
我们观察发现,每一次求i的时候只与i-1层有关,因此我们可以不用记录所有的状态,而采用滚动数组的方法来优化。这里主要讲的是另一种优化,我们发现,在每一个阶段开始的时候,实际上执行了f[i-1][] 到f[i][]的拷贝
,那么我们是不是就可以将状态空间优化为一维?f[j]
表示背包放入总体积为j的物品的最大价值。
先写出初步的等价变化
不过在第二层循环我们注意,之前我们是
f[i-1][j-w[i]] + v[i]
表我们在求第i个阶段的时候利用的是第i-1阶段的信息。现在如果有 V = 2 * w[i] 的话,那么就有f[V] = max(f[V] , f[w[i]] + v[i])
, 而f[w[i]]
是我们这个阶段刚求出来的这很明显就不符合了。而这个的解决办法也很简单,我们只需要,倒序遍历即可。
- 小技巧:如果我们令f数组都是0,而不是只有f[0] = 0 ,那么最后的结果就是
f[V]
代码:
#include<stdio.h>
#include<iostream>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = 1e5 + 10;
typedef long long LL;
int n, W;
int w[N], v[N] ;
LL f[M];
int main() {
cin >> n >> W;
for (int i = 1; i <= n ; ++i)
cin >> w[i] >> v[i];
for (int i = 1; i <= n ; ++i)
for (int j = W ; j >= w[i]; --j)
f[j] = max(f[j], f[j - w[i]] + v[i]);
LL res = 0 ;
for (int i = 1 ; i <= W ; ++i)
res = max(res, f[i]);
cout << res << endl;
return 0;
}
01背包进阶
上面点的01背包的时间复杂度是O(NM)其中N是物品个数,M是背包的容积。因此如果遇到的题目是背包的体积很大,但每一个物品的价值相对小的时候就不能用了。
我们在上面转态的表示是dp[j]:表示体积为j的物品的最大价值。那么我们就可以修改其中的含义变成:dp[i]:表示价值为i的物品的最小体积。
那么遇到这类情况我们也可以解决了。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1E5 + 10;
typedef long long LL;
int n, m ;
LL f[N] ;
int w[110], v[110];
int main() {
memset(f, 0x3f, sizeof f );
int sum = 0, res = 0;
cin >> n >> m;
for (int i = 1 ; i <= n ; ++i) {
cin >> w[i] >> v[i];
sum += v[i];
}
f[0] = 0;
for (int i = 1; i <= n ; ++i)
for (int j = sum ; j >= v[i] ; --j) {
f[j] = min(f[j], f[j - v[i]] + w[i]);
if (f[j] <= m)
res = max(j, res);
}
cout << res << endl;
return 0;
}
数字组合
之所以将这两个放在一起,是因为它们是本质相同的只不过属性发生了变化的题目。
我们先来写一些动态规划的框架
对吧惊人的相识。不过这里计算的是方案的数量。因此我们还是讲一下状态计算的部分。首先是左半部分,那么我们就直接加上其含义的方案数量即可。不过因为对于每一个阶段的
f[i][j]
值都是0,因此我们就可以写为f[i][j] = f[i-1][j]
; 对于右边的部分,我们只需要把max操作改为加即可。 利用上面提到的小技巧我们直接输出f[n][m]
就是答案了。
代码:
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 110 , M = 1e4 + 10;
int n , m ;
int a[N] , f[N][M];
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ;++i) cin>>a[i] ;
f[0][0] = 1;
for(int i = 1 ; i <= n ; ++i){
for(int j = 0 ; j <= m ; ++j)
{
f[i][j] = f[i-1][j];
if(j >= a[i])
f[i][j] += f[i-1][j-a[i]];
}
}
cout<<f[n][m]<<endl;
return 0;
}
还是考虑优化版:
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 110 , M = 1e4 + 10;
int n , m ;
int a[N] , f[M];
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ;++i) cin>>a[i] ;
f[0] = 1;
for(int i = 1 ; i <= n ; ++i)
for(int j = m ; j >= a[i] ; --j)
f[j] += f[j-a[i]];
cout<<f[m]<<endl;
return 0;
}
完全背包
完全背包与01背包的区别就是每件物品可以选无限次,只要没有超过上限的体积。
如果我们直接利用上面的01背包的倒序遍历的话,我们可以就需要这样写。(第i个物品可以选多个) 不过时间复杂度是 O ( n 2 l o n g ( n ) ) O(n^2 long(n)) O(n2long(n))是不行的。
讲优化01背包的时候,我们为1在计算第i个阶段的时候只用i-1个阶段的,因此我们从大到小来进行枚举。而对于完全背包我们只需要将倒序变成正序就可以。
#include<iostream>
using namespace std;
const int N = 1010;
int n , m, w[N] , v[N];
int f[N];
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ; ++i)cin>>w[i]>>v[i];
for(int i = 1 ; i <= n ; ++i)
for(int j = w[i] ; j <= m ; ++j )
f[j] = max(f[j] , f[j-w[i]] + v[i]);
cout<<f[m]<<endl;
return 0;
}
自然数拆分
解题思路:
我们将自然数N看成是容量为N的一个背包,那么这题就是有 1 ~N 一共N个物品 , 每一物品的体积分别从 1 ~N , 问有这些数字组合为N的所有方案的数量,不过由于可以重复使用,因此该题就是一个完全背包模型了。
- 同时这个题目是会爆int的,因此我们用
unsigned
: 2 32 − 1 2^{32} - 1 232−1. - 在c++中有一个性质就是对模上一个负数得到的结果与模上负数的绝对值(正数)其实是一样的。
陪审团
解题思路:我们要找的是和之差最小,因为题目两者之差在
−
400
−
−
−
400
-400 ---400
−400−−−400那么我们就有一个思路就是以差来划分,在计算完之后,中差为0开始依次往两边找,找到的第一个(因为对称所以一正一负)就是差最小的,在这两个之间比较找出最大值,我们通过框架梳理一下思路
还是重点来讲一下状态计算这个部分。结合背包问题的思路,对于
f[i][j][k]
我们找出最后的不同点出来以此来划分集合。首先,对于第i个人我们有选与不选两种划分。不选由含义出发就是f[i-1][j][k]
,如果选的话,
我们将第二部分,的所有情况分为变与不变两部分,对于不变的就是第i个人,那么前一个部分的含义就是:前i-1个人中,选了j-1个人,差值为 k - (p[i] - d[i]) 而这个就正好是f[i-1][j-1][k - (p[i]-d[i])]
。
- 实现上的难点:由于数组下标不能为负数,因此我们设置一个偏移量 , 对于0~399 表示负数,401~800表示正数。
- 可以剪枝的地方,因为我们知道差值只能在-400~400 之间,也就是偏移量之后的0~800.因此如果不在这个范围内的肯定不是答案。还有一个是我们在第二个方案之前j必须要大于1才可以。
- 还有一个难点是本题还要输出一个正确的方案顺序:一般这种有两个解决方法,一个是另开一个数组来存储状态的转移过程,另一个是倒推得到方案,这里我们采用倒推的解决方案。对于这个方法,我们倒推的时候是看当前这个值的一个最值是有dp公式中哪里得到的,另一个关键点是,理解倒推时候变量之间的变化。
代码:
#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<cstring>
using namespace std;
const int N = 210,M = 810 , base = 400;
int n , m ;
int ans[N] , p[N] , d[N];
int f[N][21][M];
int main(){
int T = 1;
while(scanf("%d%d",&n,&m),n&&m){
for(int i = 1 ; i <= n; ++i)scanf("%d%d",&p[i],&d[i]);
memset(f , -0x3f , sizeof f);
f[0][0][base] = 0 ;
for(int i = 1 ; i <= n ; ++i)
for(int j = 0 ; j <= m; ++j )
for(int k = 0 ; k < M ; ++k){
f[i][j][k] = f[i-1][j][k];
int t = k - (p[i] - d[i]);
if(t <0 || t >= M)continue;
if(j < 1)continue;
f[i][j][k] = max(f[i][j][k] , f[i-1][j-1][t] + p[i] + d[i]);
}
int v = 0 ;
while(f[n][m][base - v] < 0 && f[n][m][base+v] < 0)v++;
if(f[n][m][base-v] > f[n][m][base+v]) v = base - v;
else v = base + v;
int cnt = 0 , i = n , j = m , k = v;
while (j)
{
if (f[i][j][k] == f[i - 1][j][k]) i -- ;
else
{
ans[cnt ++ ] = i;
k -= (p[i] - d[i]);
i --, j -- ;
}
}
int sp = 0 , sd = 0;
for(int i = 0 ; i < cnt ; ++i) sp += p[ans[i]] , sd += d[ans[i]];
printf("Jury #%d\n",T++);
printf("Best jury has value %d for prosecution and value %d for defence:\n",sp,sd);
sort(ans, ans + cnt);
for(int i = 0 ; i < cnt ; ++i)printf(" %d",ans[i]);
puts("\n");
}
return 0;
}
我们对代码中的倒推过程来进行解释一下,首先我们明确我们是将选出的m个人的编号给输出出来,因此我们的循环条件初始条件就是m个人,只有没有到0也就是还没有记录完。 我们看dp中的公式,一个是有可能从
f[i-1][j][k]
这个状态中转移过来的,如果是的话,那么前一个状态对应的是 i - 1 , 因此要 i – 。 否则就是由f[i-1][j-1][k-(p[i] - d[i])]
得出,因为我们需要先让k-(p[i] - d[i])
,然后i-- , j--
;
多重背包
对于多重背包,我们先想一最朴素的做法应该就是将其转化为01背包问题来解决。在转化为01背包的问题上我们有两种想法:
原本的01背包框架应该是这个,一种想法是在选这一步进行改进。对于01背包我们只要选与不选两种,而对于多重背包在选这个基础上我们可能选1… c[i]个。那么就有
因此就有:
#include<iostream>
using namespace std;
const int N = 110;
int f[N] , n , m;
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ; ++i){
int w , v , s;
cin>>w>>v>>s;
for(int j = m ; j >= w ; --j)
// k * w <= j , 即我选了这些后的前一个状态不可能是小于0的
for(int k = 1 ; k <= s && k * w <= j ; k++)
f[j] = max(f[j] , f[j- k * w] + k * v);
}
cout<<f[m]<<endl;
return 0;
}
还有一种思路是我将这个k * w[i[,分为k次01背包,也就是说将其拆分成一个一个的,然后利用01背包的写法求解。这两种是等价的做法。
代码:
#include<iostream>
using namespace std;
const int N = 110;
int f[N] , n , m;
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ; ++i){
int w , v , s;
cin>>w>>v>>s;
for(int k = 1 ; k <= s ; ++k)
for(int j = m ; j >= w ; --j)
f[j] = max(f[j] , f[j- w] + v);
}
cout<<f[m]<<endl;
return 0;
}
二进制优化版
首先我们回想一下上面直接拆分为01背包的问题,我们是将其拆为一个一个的物品,因此我们的复杂度很高。这里我们要明确说明一个问题?对于0~n 的数,我们需要使用多少的数才可以将它们表示出来,当然肯定可以,当是最笨的一种方法就是n个1 ,如果一个都不选就表示0,选n个1就表示n。不过这显然就想我们上面的拆分方式就不可取。这里我们有一个结论:至少需要 log 2 n \log_{2}{n} log2n这么一个数量的数字。这什么实现呢?好比10: 1 2 4 8 。这显然是不可以的,因为这就能表示0~15了大于10了。因此我们对于不超过 2 k 2^k 2k的话就直接用剩下就好,就是:1 , 2 ,4,3。这就可以了。
代码:
#include<stdio.h>
#include<iostream>
#include<vector>
using namespace std;
const int N = 2010;
int n , m ,f[N];
struct GOOD{
int v,w;
};
int main(){
cin>>n>>m;
vector<GOOD>goods;
for(int i = 1 ; i <= n ; ++i){
int v ,w, s;
cin>>w>>v>>s;
for(int k = 1 ; k <= s ; k *=2){
s -= k;
goods.push_back({k*v, k *w});
}
if(s > 0) goods.push_back({s*v,s*w});
}
for(auto good : goods){
for(int j = m ; j >= good.w ; --j)
f[j] = max(f[j] , f[j - good.w] + good.v);
}
cout<<f[m]<<endl;
return 0;
}
单调队列优化版
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1010 , M = 20010;
int n , m;
int f[M] , g[M] ,q[M];
int main(){
cin>>n>>m;
for(int i = 1 ; i <= n ; ++i){
memcpy(g , f ,sizeof f);
int v , w , s;
cin>>v>>w>>s;
for(int j = 0 ; j < v ; ++j){
int h = 0 ,t = -1;
for(int k = j ; k <= m ; k += v){
if(h <= t && q[h] < k - s*v )h++;// 【k-s*v ,k-v】
//g[q[h]] 当前体积(q[h])下的最大价值,(k - q[h])/v*w 在k下,除了q[h]外还能放的价值
if(h <= t)f[k] = max(g[k] , g[q[h]] + (k - q[h])/v*w);//在g中找的,
while(h <= t && g[k] >= g[q[t]] + (k - q[t])/v*w)t--;
q[++t] = k;
}
}
}
cout<<f[m]<<endl;
return 0;
}
分组背包
题目转送们
解题思路
这题可以说是01背包模型的拓展吧,与01背包有一点不同的是,我们在选择一堆物品的时候需要枚举这一推物品中的所有物品后只选择一个。我么01的是这一推物品是否选,如果选的话只能在这一推物品中选一个。也可以从另一种角度来想,多重背包是这类题的一个特殊情况,对于多重背包我们可以将一个物品与它的数量看成一堆物品。在这一堆物品中,分别为1个该物品,…,s个该物品。
代码:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int n , m ;
int w[N] , v[N] ;
int f[N];
int main(){
cin>>n>>m;
for(int i = 1 ; i <=n ; ++i){ // 第几组
int s;
cin>>s;
for(int i = 1 ; i <= s ; ++i)cin>>w[i]>>v[i];
for(int j = m ; j >= 1 ; j--) // 体积
for(int k = 1 ; k <= s; k++) // 选哪个物品
if(j >= w[k])f[j] = max(f[j] , f[j-w[k]] + v[k]);
}
cout<<f[m]<<endl;
return 0;
}
区间DP
在区间DP中,一个状态由若干个比它小且包含与它的区间所代表的状态转移过来。区间DP一般是枚举区间长度,左端点,和划分区间的位置。(因为右端点可以由区间长度和左端点得到)。
环形石子和并
解题思路:这题关键点是,只能合并相邻的石子,因此我们就能知道如果我们能合并在l和r位置的两个石子堆,说明这中间的的石子堆已经合并完全了。以下是分析过程:(我们通过分析最大花费来展示)
我们还是来解释一下状态计算,对于这个问题我们不好直接计算,那么我们就划分为子问题,分而治之就简单了。对于所有和并L和R的所有方案,我们想一下它们的最后的不同点,不同点应该是中间以哪一个位置k作为划分点将[L,R]区间分为了[L,K],和[K+1,R]两个部分,因此我们要做的就是枚举k的位置。同时我们在合并一个区间
[L,R]
的时候都需要从L到R之间数的和的一个代价,这一步我们就可以利用前缀个来进行优化。
- 对于环状我们一般是将其拆分成长度为2n的一个链式。只有我们的左右端点就会发生改变,之后我们遍历一边
[fi,n+i-1]
(意思是在第i处将环断开)取出一个最大值。
代码:
int n;
int a[MAXN];
int g[MAXN][MAXN],f[MAXN][MAXN]; //最大和最小
int sum[MAXN];
int main(){
scanf("%d",&n);
for(int i =1;i<=n;i++){
scanf("%d",&a[i]);
a[n+i] = a[i]; //构造
}
for(int i=1;i<=2*n;i++){ //前缀和
sum[i] =sum[i-1] + a[i];
}
memset(g,0x3f,sizeof(g));
memset(f,-0x3f,sizeof(f));
for(int lena=1;lena<=n;lena++){
for(int l=1;l+lena-1 <=n*2;l++){
int r = l + lena -1;
if(l == r){
g[l][r] = f[l][r] = 0;
}
else {
for(int k = l;k < r;k++){
g[l][r] = min(g[l][r],g[l][k]+g[k+1][r] + (sum[r] - sum[l-1]));
f[l][r] = max(f[l][r],f[l][k]+f[k+1][r] + (sum[r] - sum[l-1]));
}
}
}
}
int maxn = -INF,minx = INF;
for(int i=1;i<=n;i++){ //看n种分割哪种最大和最小
minx = min(minx,g[i][i+n-1]);
maxn = max(maxn,f[i][i+n-1]);
}
printf("%d\n%d\n",minx,maxn);
return 0;
}