0x51 线性DP
LIS
最长上升子序列,给定一个长度为\(N\)序列\(A\),求出数值单调递增的子序列长度最长是多少
\(O(n^2)\)做法
\(f[i]\)表示以\(i\)结尾的最长上升子序列长度是多少
自然转移方程为\(f[i]=max(f[j])+1,(1\le j < i,A[j]<A[i] )\)
for( register int i = 1 ; i <= n ; i ++ )
{
f[i] = 1;
for( register int j = 1 ; j < i ; j ++)
{
if( a[j] >= a[i] ) continue;
f[i] = max( f[i] , f[j] + 1 );
}
}
\(O(nlog_n)\)做法
对于\(O(n^2)\)的做法,我们每次都枚举
假设我们已经求的一个最长上升子序列,我们要进行转移,如果对于每一位,在不改变性质的情况下,每一位越小,后面的位接上去的可能就越大,所以对于每一位如果大于末尾一位,就把他接在末尾,否则在不改变性质的情况下,把他插入的序列中
for( register int i = 1 ; i <= n ; i ++ )
{
if( a[i] > f[ tot ] ) f[ ++ tot ] = a[i];
else *upper_bound( f + 1 , f + 1 + tot , a[i] ) = a[i];
}
//tot就是LIS的长度
这种做法的缺点是不能求出每一位的\(LIS\),注意最后的序列并不是\(LIS\),只是长度是\(LIS\)的长度
输出路径
\(O(nlog_n)\)的方法无法记录路径,所以考虑在\(O(n^2)\)的方法上进行修改,其实就是记录路径
inline void output( int x )//递归输出
{
if( x == from[x] )
{
printf("%d " , a[x] );
return ;
}
output( from[x] );
printf("%d " , a[x] );
return ;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; i ++ ) a[i] = read();
for( register int i = 1 ; i <= n ; i ++ )
{
f[i] = 1 , from[i] = i;
for( register int j = 1 ; j < i ; j ++ )
{
if( a[j] >= a[i] || f[j] + 1 <= f[i] ) continue;
f[i] = f[j] + 1;
from[i] = j;
}
}
for( register int i = 1 ; i <= n ; i ++ ) output( i ) , puts("");
//这中做法不仅可以得到路径,还可以得到每一个前缀的LIS的路径
return 0;
}
LCS
Luogu P3902 递增
最长上升子序列问题的模板题,求出当前序列的最长上升子序列长度
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n , a[N] , tot;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
n = read();
for( register int i = 1 , x ; i <= n ; i ++ )
{
x = read();
if( x > a[ tot ] ) a[ ++ tot ] = x;
else * upper_bound( a + 1 , a + 1 + tot , x ) = x;
}
cout << n - tot << endl;
}
这里题目要求的是严格递增,如果要求递增可以把upper_bound换成lower_bound
AcWing 271. 杨老师的照相排列
这道题的题面非常的绕
其实就是让你放\(1\cdots N\)个数,每一行,每一列都是递增的
显然放的顺序是没有影响的,那我们不妨从\(1\)到\(N\)逐个放
首先先找一些性质
\(1\)一定放在\((1,1)\)上,不然不能满足递增
我们在放每一行的时候,必须要从左到右挨个放,显然在放\(x\)的时候,如果\(x-1\)没有放,那么在以后放\(x-1\)这个位置的数一定会比\(x\)更大
我们在放\((i,j)\)时,\((i+1,j)\)一定不能放,同理也是无法满足递增
有了这些性质我们就可以设计转移了
我们用\(f[a,b,c,d,e]\)来表示地几行放了几个数
如果a && a - 1 >=b,那么\(f[a,b,c,d,e]\)可以由\(f[a-1,b,c,d,e]\)转移来,即f[a][b][c][d][e]+=f[a-1][b][c][d][e]
同理如果b && b - 1 >= c,那么就有f[a][b][c][d][e]+=f[a][b-1][c][d][e]
同理可得其他三种情况
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 35;
int n , s[6];
LL f[N][N][N][N][N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
for( ; ; )
{
n = read();
if( !n ) break;
memset( s , 0 , sizeof( s ) );
for( register int i = 1 ; i <= n ; i ++ ) s[i] = read();
memset( f , 0 , sizeof( f ) );
f[0][0][0][0][0] = 1;
for( register int a = 0 ; a <= s[1] ; a ++ )
{
for( register int b = 0 ; b <= min( a , s[2] ) ; b ++ )
{
for( register int c = 0 ; c<= min( b , s[3] ) ; c ++ )
{
for( register int d = 0 ; d <= min( c , s[4] ) ; d ++ )
{
for( register int e = 0 ; e <= min( d , s[5] ) ; e ++ )
{
register LL &t = f[a][b][c][d][e];
if( a && a - 1 >= b ) t += f[ a - 1 ][b][c][d][e];
if( b && b - 1 >= c ) t += f[a][ b - 1 ][c][d][e];
if( c && c - 1 >= d ) t += f[a][b][ c - 1 ][d][e];
if( d && d - 1 >= e ) t += f[a][b][c][ d - 1 ][e];
if( e ) t += f[a][b][c][d][ e - 1 ];
}
}
}
}
}
cout << f[ s[1] ][ s[2] ][ s[3] ][ s[4] ][ s[5] ] << endl;
}
return 0;
}
AcWing 272. 最长公共上升子序列
设计状态转移方程
f[i][j]表示\(a[1\cdots i]\)和\(b[1\cdots j]\)中以\(b[j]\)为结尾的最长公共子序列
那么就可以得到
\[
f[i][j]=\left\{ \begin{matrix} f[i-1][j],(a[i]!= b[j])\\max(f[i-1][1\le k < j]),(a[i]==b[j] ,a[i]>b[k]) \end{matrix}\right.
\]
那么这个算法的实现就很简单
for( register int i = 1 ; i <= n ; i ++ )
{
for( register int j = 1 ; j <= n ; j ++ )
{
f[i][j] = f[i-1][j];
if( a[i] == a[j] )
{
register int maxv = 1;
for( register int k = 1 ; k < j ; k ++ )
if( a[i] > b[k] ) maxv = max( maxv , f[ i - 1 ][k] );
f[i][j] = max( f[i][j] , maxv + 1 );
}
}
}
然后我们发现maxv的值与i无关,只与j有关
所以我们可以吧求maxv过程提出来,这样复杂度就降到了\(O(n^2)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 3010;
int n , a[N] , b[N] , f[N][N];
inline int read()
{
register int x = 0 , f = 1;
register char ch = getchar();
while( ch < '0' || ch > '9' )
{
if( ch == '-' ) f = -1;
ch = getchar();
}
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x * f ;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; i ++ ) a[i] = read();
for( register int i = 1 ; i <= n ; i ++ ) b[i] = read();
for( register int i = 1 , maxv ; i <= n ; i ++ )
{
maxv = 1;
for( register int j = 1 ; j <= n ; j ++ )
{
f[i][j] = f[ i - 1 ][j];
if( a[i] == b[j] ) f[i][j] = max( f[i][j] , maxv );
if( a[i] > b[j] ) maxv = max( maxv , f[ i - 1 ][ j ] + 1 );
}
}
register int res = -1;
for( register int i = 1 ; i <= n ; i ++ ) res = max( res , f[n][i] );
cout << res << endl;
}
AcWing 273. 分级
对于单调递增和单调递减的情况我们分别求一下,取较小值即可
这里只考虑单调递增的情况
先来一个引理
一定存在一组最优解,使得\(B_i\)中的每个元素都是\(A_i\)中的某一个值
证明如下
横坐标\(A_i\)表示原序列,\(A`_i\)表示排序后的序列,红色圈代表\(B_i\)
粉色框里框住的圈,是\(B_i\)不是\(A_i\)中某个元素的情况,当前状态是一个解
我们统计出\(A_2\cdots A_4\)中大于\(A`_1\)的个数\(x\)和小于\(A`_1\)的个数\(y\)
如果\(x>y\),我们将框内的元素向上平移,直到最高的一个碰到\(A`_2\),结果会变得更优
如果\(x<y\),我们将框内的元素向下平移,直到最高的一个碰到\(A`_1\),结果会变得更优
如果\(x=y\),向上向下都可以,结果不会变差
我们可以通过这种方式得到一组符合引里的解
换言之我们只要从\(A_i\)找到一个最优顺序即可
那么考虑状态转移方程
f[i][j]表示长度为i且B[i]=A'[j]的最小值
考虑B[i-1]的范围是A'[1]~A[j],所以f[i][j]=min(f[i-1][1~j])+abs(A[i]-B[j])
为什么这里可以取到j呢,注意题目上说的是非严格单调的,所以B[i]==B[i-1]是合法的
#include <bits/stdc++.h>
using namespace std;
const int N = 2005 , INF = 0x7f7f7f7f;
int n , a[N] , b[N] , f[N][N] , res;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline int work()
{
for( register int i = 1 , minv = INF ; i <= n ; i ++ , minv = INF )
{
for( register int j = 1 ; j <= n ; j ++ )
{
minv = min( minv , f[ i - 1 ][j] );
f[i][j] = minv + abs( a[i] - b[j] );
}
}
register int ans = INF;
for( register int i = 1 ; i <= n ; i ++ ) ans = min( ans , f[n][i] );
return ans;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; i ++ ) a[i] = b[i] = read();
sort( b + 1 , b + 1 + n );
res = work();
reverse( a + 1 , a + 1 + n );
printf( "%d\n" , min( res , work() ) );
return 0;
}
AcWing 274. 移动服务
当我们看到的题目的第一反应是设状态方程f[x][y][z]表示三个人分别在x,y,z上
但是我们发现我们并不知道最终的状态是是什么,因为只知道有一个人在p[n]上其他的都不知道,所以更换思路
我们设f[i][x][y]表示三个人分别在p[i],x,y位置上,这样最终状态就是f[n][x][y]我们只要找到最小的一个即可
转移很简单就是枚举三个人从p[i]走到p[i+1]即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1005 , M = 205 , INF = 0x7f7f7f7f;
int n , m , dis[M][M] , q[N] , f[N][M][M] , res = INF;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
m = read() , n = read();
for( register int i = 1 ; i <= m ; i ++ )
{
for( register int j = 1 ; j <= m ; j ++ ) dis[i][j] = read();
}
for( register int i = 1 ; i <= n ; i ++ ) q[i] = read(); q[0] = 3;
memset( f , INF , sizeof( f ) ) , f[0][1][2] = 0;
for( register int i = 0 ; i < n ; i ++ )
{
for( register int x = 1 ; x <= m ; x ++ )
{
for( register int y = 1 ; y <= m ; y ++ )
{
register int z = q[i] , v = f[i][x][y];
if( x == y || z == x || z == y ) continue;
register int u = q[ i + 1 ];
f[ i + 1 ][x][y] = min( f[ i + 1 ][x][y] , v + dis[z][u] );
f[ i + 1 ][z][y] = min( f[ i + 1 ][z][y] , v + dis[x][u] );
f[ i + 1 ][x][z] = min( f[ i + 1 ][x][z] , v + dis[y][u] );
}
}
}
for( register int i = 1 ; i <= m ; i ++ )
{
for( register int j = 1 ; j <= m ; j ++ )
{
if( i == j || i == q[n] || j == q[n] ) continue;
res = min( res , f[n][i][j] );
}
}
cout << res << endl;
return 0;
}
到这里已经可以过这道题了
我们考虑还能怎么优化,显然时间复杂度很难优化了,下面介绍一个常用的东西
滚动数组优化动态规划空间复杂度
我们发现整个DP状态转移中,能够影响f[i+1][x][y]的只有f[i][x][y]所以我们没有必要存f[i-1][x][y]之前的状态所以只保存两个状态即可
我们用两个值to和now来表示状态的下标,每次转移后交换两个值即可,这样可以减少大部分的空间
#include <bits/stdc++.h>
#define exmin( a , b , c , d ) ( min( min( a , b ) , min( c , d ) ) )
using namespace std;
const int N = 1005 , M = 205 , INF = 0x7f7f7f7f;
int n , m , dis[M][M] , q[N] , f[2][M][M] , res = INF , to , now;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
m = read() , n = read();
for( register int i = 1 ; i <= m ; i ++ )
{
for( register int j = 1 ; j <= m ; j ++ ) dis[i][j] = read();
}
for( register int i = 1 ; i <= n ; i ++ ) q[i] = read(); q[0] = 3;
now = 0 , to = 1 ;
memset( f[now] , INF , sizeof( f[now] ) );
f[now][1][2] = 0;
for( register int i = 0 ; i < n ; i ++ )
{
memset( f[to] , INF , sizeof( f[to] ) );
for( register int x = 1 ; x <= m ; x ++ )
{
for( register int y = 1 ; y <= m ; y ++ )
{
register int z = q[i] , v = f[now][x][y];
if( x == y || z == x || z == y ) continue;
register int u = q[ i + 1 ];
f[ to ][x][y] = min( f[ to ][x][y] , v + dis[z][u] );
f[ to ][z][y] = min( f[ to ][z][y] , v + dis[x][u] );
f[ to ][x][z] = min( f[ to ][x][z] , v + dis[y][u] );
}
}
swap( to , now );
}
for( register int i = 1 ; i <= m ; i ++ )
{
for( register int j = 1 ; j <= m ; j ++ )
{
if( i == j || i == q[n] || j == q[n] ) continue;
res = min( res , f[now][i][j] );
}
}
cout << res << endl;
return 0;
}
0x52 背包问题
0/1背包
给定\(N\)个物品,其中第\(i\)个物品的体积为\(V_i\),价值为\(W_i\)。有一个容积为\(M\)的背包,要求选择一些物品放入背包,在体积不超过\(M\)的情况下,最大的价值总和是多少
我们设f[i][j]表示前i个物品用了j个空间能获得的最大价值,显然可以得到下面的转移
int f[N][M];
for( register int i = 1 ; i <= n ; i ++ )
{
for( register int j = v[i] ; j <= m ; j ++ )
{
f[i][j] = f[ i - 1 ][j];
f[i][j] = max( f[i][j] , f[ i - 1 ][ j - v[i] ] + w[i] );
}
}
int ans = 0;
for( register int i = 0 ; i <= m ; i ++ ) ans = max( ans , f[n][i] );
显然可以用滚动数组优化空间,得到下面的代码
int f[2][M];
for( register int i = 1 ; i i <= n ; i ++ )
{
for( register int j = v[i] ; j <= m ; j ++ )
{
f[ i & 1 ][j] = f[ ( i - 1 ) & 1 ][j];
f[ i & 1 ][j] = max( f[ i & 1 ][j] , f[ ( i - 1 ) & 1 ][ j - v[i] ] + w[i] );
}
}
int ans = 0;
for( register int i = 0 ; i <= m ; i ++ ) ans = max( ans , f[ n & 1 ][i] );
当这并不是常规的写法,下面这种才是常规的写法
int f[M];
for( register int i = 1 ; i <= n ; i ++ )
{
for( register int j = m ; j >= v[i] ; j -- )
{
f[j] = max( f[j] , f[ j - v[i] ] + w[i] );
}
}
int ans = 0;
for( register int i = 0 ; i <= m ; i ++ ) ans = max( ans , f[i] );
但是我们发现在枚举空间的时候是倒序枚举,是为了防止多次重复的选择导致不合法
举个例子你先选到f[v[i]]会选择一次物品i,但当选择到f[2*v[i]]时就会再选择一次,显然是不合法的
所以要记住\(0/1\)背包的这一重点,要倒序枚举空间
AcWing 278. 数字组合
按照\(0/1\)背包的模型求解方案数即可
int main()
{
n = read() , m = read() , f[0] = 1;
for( register int i = 1 , v ; i <= n ; i ++ )
{
v = read();
for( register int j = m ; j >= v ; j -- ) f[j] += f[ j - v ];
}
cout << f[m] << endl;
return 0;
}
Luogu P1510 精卫填海
这也是一个经典的背包问题,背包求最小费用
f[i][j]表示前\(i\)个物品用了\(j\)的体力所能填满的最大空间,显然滚动数组优化一维空间
然后枚举一下体力,找到最先填满全部体积的一个即可
简单分析一下,当花费的体力增加时,所填满的体积保证不会减小,满足单调性
二分查找会更快
#include<bits/stdc++.h>
using namespace std;
const int N = 10005;
int n , V , power ,f[N] , use;
bool flag = 0;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
V = read() , n = read() , power = read();
for( register int i = 1 , v , w ; i <= n ; i ++ )
{
v = read() , w = read();
for( register int j = power ; j >= w ; j -- ) f[j] = max( f[j] , f[ j - w ] + v );
}
use = lower_bound( f + 1 , f + 1 + power , V ) - f;
if( f[use] >= V ) printf( "%d\n" , power - use );
else puts("Impossible");
return 0;
}
Luogu P1466 集合 Subset Sums
结合一点数学知识,\(\sum_{x=1}^xx=\frac{(x+1)x}{2}\)
要把这些数字平均分成两部分,那么两部分的和一定是\(\frac{(x+1)x}{4}\)
剩下就是一个简单的背包计数模板
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 400;
LL n , m , f[N];
int main()
{
cin >> n;
if( n * ( n + 1) % 4 )
{
puts("0");
exit(0);
}
m = n * ( n + 1 ) / 4;
f[0] = 1;
for( register int i = 1 ; i <= n ; i ++ )
{
for( register int j = m ; j >= i ; j -- ) f[j] += f[ j - i ];
}
cout << f[m] / 2 << endl;
}
完全背包
给定\(N\)种物品,其中第\(i\)个物品的体积为\(V_i\),价值为\(W_i\),每个物品有无数个。有一个容积为\(M\)的背包,要求选择一些物品放入背包,在体积不超过\(M\)的情况下,最大的价值总和是多少
这里你会发现完全背包和\(0/1\)背包的差别就只剩下数量了,所以代码也基本相同,只要把枚举容量改成正序循环即可
int f[M];
for( register int i = 1 ; i <= n ; i ++ )
{
for( register int j = v[i] ; j <= m ; j ++ )
{
f[j] = max( f[j] , f[ j - v[i] ] + w[i] );
}
}
int ans = 0;
for( register int i = 0 ; i <= m ; i ++ ) ans = max( ans , f[i] );
AcWing 279. 自然数拆分
直接套用完全背包的模板并将max函数改成求和即可
#include <bits/stdc++.h>
using namespace std;
const int N = 4005;
unsigned int n , f[N];
int main()
{
cin >> n;
f[0] = 1;
for( register int i = 1 ; i <= n ; i ++ )
{
for( register int j = i ; j <= n ; j ++ )
{
f[j] = ( f[j] + f[ j - i ] ) % 2147483648u;
}
}
cout << ( f[n] > 0 ? f[ n ] - 1 : 2147483648u ) << endl;
return 0;
}
P2918 买干草Buying Hay
类似P1510精卫填海,不过这是完全背包稍作修该即可
不过要注意f[need]并非最优解,因为可以多买点,只要比需要的多即可
#include <bits/stdc++.h>
using namespace std;
const int N = 505005 , INF = 0x7f7f7f7f;
int n , need , f[N] , ans = INF ;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
n = read() , need = read();
memset( f , INF , sizeof(f) ) , f[0] = 0;
for( register int i = 1 , w , v ; i <= n ; i ++ )
{
v = read() , w = read();
for( register int j = v ; j <= need + 5000; j ++ ) f[j] = min( f[j] , f[ j - v ] + w ) ;
}
for( register int i = need ; i <= need + 5000 ; i ++ ) ans = min( ans , f[i] );
cout << ans << endl;
return 0;
}
多重背包
给定\(N\)种物品,其中第\(i\)个物品的体积为\(V_i\),价值为\(W_i\),每个物品有\(K_I\)个。有一个容积为\(M\)的背包,要求选择一些物品放入背包,在体积不超过\(M\)的情况下,最大的价值总和是多少
这里的多重背包还是对物品的数量进行了新的限制,限制数量,其实做法和\(0/1\)背包差不多,只要增加一维枚举数量即可
二进制拆分优化
众所周知,我们可以用\(2^0,2^1,2^2\cdots,2^{k-1}\),这\(k\)个数中任选一些数相加构造出\(1\cdots 2^k-1\)中的任何一个数
所以我们就可以把多个物品拆分成多种物品,做\(0/1\)背包即可
二进制拆分的过程
for( register int i = 1 , wi , vi , ki , p ; i <= n ; i ++ )
{
wi = read() , vi = read() , ki = read() , p = 1;
while( ki > p ) ki -= p , v[ ++ tot ] = vi * p , w[ tot ] = wi * p , p <<= 1;
if( ki > 0 ) v[ ++ tot ] = vi * ki , w[ tot ] = wi * ki;
}
Luogu P1776 宝物筛选
这是一道,经典的模板题,直接套板子即可
#include <bits/stdc++.h>
using namespace std;
const int N = 12e5 + 5;
int n , m , v[N] , w[N] , f[N] , tot ;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
n = read() , m = read();
for( register int i = 1 , vi , ki , wi , p ; i <= n ; i ++ )
{
wi = read() , vi = read() , ki = read() , p = 1;
while( ki > p ) ki -= p , w[ ++ tot ] = wi * p , v[tot] = vi * p , p <<= 1 ;
if( ki ) w[ ++ tot ] = wi * ki , v[tot] = vi * ki;
}
for( register int i = 1 ; i <= tot ; i ++ )
{
for( register int j = m ; j >= v[i] ; j -- ) f[j] = max( f[j] , f[ j - v[i] ] + w[i] );
}
cout << f[m] << endl;
return 0;
}
分组背包
给定\(N\)组物品,每组内有多个不同的物品,每组的物品只能挑选一个。在背包容积确定的情况下求最大价值总和
其实是\(0/1\)背包的一种变形,结合伪代码理解即可
for( i /*枚举组*/)
{
for( j /*枚举容积*/)
{
for(k/*枚举组内物品*/)
{
f[j] = max( f[j] , f[ j - v[i][k] ] + w[i][k] );
}
}
}
记住一定要先枚举空间这样可以保证每组物品只选一个
Luogu P1757 通天之分组背包
模板题,套板子即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1005 , M = 105;
int n , m , v[N] , w[N] , f[N] , tot = 0;
vector< int > g[M];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
m = read() , n = read();
for( register int i = 1 , a , b , c ; i <= n ; i ++ )
{
v[i] = read() , w[i] = read() , c = read();
g[c].push_back( i );
tot = max( tot , c );
}
for( register int i = 1 ; i <= tot ; i ++ )
{
for( register int j = m ; j >= 0 ; j -- )
{
for( auto it : g[i] )
{
if( j < v[it] ) continue;
f[j] = max( f[j] , f[ j - v[it] ] + w[it] );
}
}
}
cout << f[m] << endl;
return 0;
}
混合背包
混合背包其实,一部分是\(0/1\)背包,完全背包,多重背包混合
for ( /*循环物品种类*/ ) {
if (/*是 0 - 1 背包*/ )
/* 套用 0 - 1 背包代码*/;
else if ( /*是完全背包*/)
/*套用完全背包代码*/;
else if (/*是多重背包*/)
/*套用多重背包代码*/;
}
有一种做法是利用二进制拆分全部转发成\(0/1\)背包来做,如果完全背包,就通过考虑上限的方式进行拆分,因为背包的容积是有限的,根据容积计算出最多能取多少个
如果只有\(0/1\)背包和完全背包可以判断一下,\(0/1\)背包倒着循环,完全背包正着循环
Luogu P1833 樱花
0x53 区间DP
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来由很大的关系。令状态 表示将下标位置 到 的所有元素合并能获得的价值的最大值,那么 , 为将这两组元素合并起来的代价。
区间 DP 的特点:
合并 :即将两个或多个部分进行整合,当然也可以反过来;
特征 :能将问题分解为能两两合并的形式;
求解 :对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
AcWing 282. 石子合并
f[l][r]表示将$l\cdots r $ 的石子合并成一堆所需要的最小代价
因为每次只能合并两堆石子,所以我们可以枚举两堆石子的分界点,这应该是区间\(DP\)最简单的一道题了吧
#include <bits/stdc++.h>
using namespace std;
const int N = 305 , INF = 0x7f7f7f7f;
int n , a[N] , s[N] , f[N][N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; a[i] = read() , s[i] = s[ i - 1 ] + a[i] , i ++ );
for( register int len = 2 ; len <= n ; len ++ )
{
for( register int l = 1 , r = len ; r <= n ; l ++ , r ++ )
{
f[l][r] = INF;
for( register int k = l ; k < r; k ++ ) f[l][r] = min( f[l][r] , f[l][k] + f[k+1][r] + s[r] - s[ l - 1 ] );
}
}
cout << f[1][n] << endl;
return 0;
}
Luogu P4170 涂色
设f[l][r]为将l到r全部涂成目标状态的最下代价
显然当l == r时,f[l][r] = 1
当l != r且opt[l] == opt[r]时,只需在涂抹中间区间时多涂抹一格,即f[l][r] = min( f[l+1][r] , f[l][r-1 ] )
当l != r且opt[l] != opt[r]时,考虑分成两个区间来涂抹,即f[l][r]=min( f[l][k] + f[k+1][r] )
设计好状态转移后,按照套路枚举区间长度,枚举左端点,转移即可
#include <bits/stdc++.h>
using namespace std;
const int N = 55 , INF = 0x7f7f7f7f;
int n , f[N][N];
string opt;
int main()
{
cin >> opt;
n = opt.size();
for( register int i = 1 ; i <= n ; i ++ ) f[i][i] = 1;
for( register int len = 2 ; len <= n ; len ++ )
{
for( register int l = 1 , r = len ; r <= n ; l ++ , r ++ )
{
if( opt[ l - 1 ] == opt[ r - 1 ] ) f[l][r] = min( f[ l + 1 ][r] , f[l][ r - 1 ] );
else
{
f[l][r] = INF;
for( register int k = l ; k < r ; k ++ ) f[l][r] = min( f[l][r] , f[l][k] + f[ k + 1 ][r] );
}
}
}
cout << f[1][n] << endl;
return 0;
}
Luogu P3205 合唱队
f[l][r][0/1]表示站成目标队列l到r部分,且最后一个人在左或右边的方案数
根据题目的要求稍作思考就能得到转移方程
f[l][r][0] = f[ l + 1 ][r][0] * ( h[l] < h[ l + 1 ] ) + f[ l + 1 ][r][1] * ( h[l] < h[r] ) ; f[l][r][1] = f[l][ r - 1 ][0] * ( h[r] > h[l] ) + f[l][ r - 1 ][1] * ( h[r] > h[ r - 1 ] ) ;
#include<bits/stdc++.h>
using namespace std;
const int N = 1005 , mod = 19650827;
int n , h[N] , f[N][N][2];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; h[i] = read() , f[i][i][0] = 1 , i ++ );
for( register int len = 2 ; len <= n ; len ++ )
{
for( register int l = 1 , r = len ; r <= n ; l ++ , r ++ )
{
f[l][r][0] = ( f[ l + 1 ][r][0] * ( h[l] < h[ l + 1 ] ) + f[ l + 1 ][r][1] * ( h[l] < h[r] ) ) % mod;
f[l][r][1] = ( f[l][ r - 1 ][0] * ( h[r] > h[l] ) + f[l][ r - 1 ][1] * ( h[r] > h[ r - 1 ] ) ) % mod;
}
}
cout << ( f[1][n][0] + f[1][n][1] ) % mod << endl;
return 0;
}
Loj 10148. 能量项链
这道题因为是环,直接处理很复杂,用到一个常用的技巧破环成链
简单说就是把环首未相接存两边,剩下的就是区间\(DP\)了模板了
#include<bits/stdc++.h>
using namespace std;
const int N = 205;
int n , head[N] , tail[N] , f[N][N] , ans;
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; head[i] = head[ i + n ] = read() , i ++ );
for( register int i = 1 ; i < 2 * n ; tail[i] = head[ i + 1 ] , f[i][i] = 0 , i ++ ); tail[ n * 2 ] = head[1];
for( register int len = 2 ; len <= n ; len ++ )
{
for( register int l = 1 , r = len ; r < n * 2 ; l ++ , r ++ )
{
for( register int k = l ; k < r ; k ++ ) f[l][r] = max( f[l][r] , f[l][k] + f[ k + 1][r] + head[l] * tail[k] * tail[r] );
if( len == n ) ans = max( ans , f[l][r] );
}
}
cout << ans << endl;
return 0;
}
说到破环成链,前面的石子合并其实也是要破环成链的,但数据太水了,Loj 10147.石子合并题目不变当数据比上面加强了,需要考虑破环成链
0x54 树形DP
树形\(DP\),即在树上进行的\(DP\)。由于树固有的递归性质,树形\(DP\)一般都是递归进行的。
Luogu P1352 没有上司的舞会
定义f[i][0/1]\(表示以\)i\(为根节点是否选择\)i$的最有解
显然可以得到下面的转移方程,其中v表示i的子节点
f[i][1] += f[v][0]
f[i][0] += max( f[v][1] , f[v][0] )
然后我们发现这样似乎很难递推做,所以大多数时候的树形\(DP\)都是用\(DFS\)来实现的
#include <bits/stdc++.h>
#define PII pair< int , int >
#define v first
#define next second
using namespace std;
const int N = 6005;
int n , w[N] , root , head[N] , tot , f[N][2];
bool st[N];
PII e[N];
inline int read()
{
register int x = 0 , f = 1 ;
register char ch = getchar();
while( ch < '0' || ch > '9')
{
if( ch == '-' ) f = -1;
ch = getchar();
}
while( ch >= '0' && ch <= '9' )
{
x = ( x << 1 ) + ( x << 3 ) + ch - '0';
ch = getchar();
}
return x * f;
}
inline void add( int u , int v )
{
e[++tot] = { v , head[u] } , head[u] = tot;
}
inline void dfs( int u )
{
f[u][1] = w[u];
for( register int i = head[u] ; i ; i = e[i].next )
{
dfs( e[i].v );
f[u][0] += max( f[ e[i].v ][1] , f[ e[i].v ][0] );
f[u][1] += f[ e[i].v ][0];
}
return ;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; w[i] = read() , i ++ );
for( register int i = 1 , u , v ; i < n ; i ++ )
{
v = read() , u = read();
add( u , v ) , st[v] = 1;
}
for( root = 1 ; st[root] ; root ++ );
dfs( root );
cout << max( f[root][1] , f[root][0] ) << endl;
return 0;
}
AcWing 286. 选课
这是一道依赖条件的背包,可以当作是在树上做背包
因为每个子树之间没有横插边,所以每个子树是相互独立的
所以当前节点的最大值就是子树最大值之和加当前节点的权值
我们给任意一个子树分配任意的空间,不过每个子树只能用一次,所以这里用到呢分组背包的处理
原图不保证是一颗树,所以可能是一个森林,建立一个虚拟节点,权值为\(0\)和每个子树的根节点相连,这样就构成一颗完整的树,这里把\(0\)当作了虚拟节点
#include <bits/stdc++.h>
#define PII pair< int , int >
#define v first
#define next second
#define son e[i].v
using namespace std;
const int N = 305;
int n , m , w[N] , head[N] , tot = 0, f[N][N];
PII e[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline void add( int u , int v )
{
e[ ++ tot ] = { v , head[u] } , head[u] = tot;
return ;
}
inline void dfs( int u )
{
for( register int i = head[u] ; i != -1 ; i = e[i].next )
{
dfs( son );
for( register int j = m - 1 ; j >= 0 ; j -- )
{
for( register int k = 1 ; k <= j ; k ++ )
{
f[u][j] = max( f[u][j] , f[u][ j - k ] + f[ son ][ k ] );
}
}
}
for( register int j = m ; j ; j -- ) f[u][j] = f[u][ j - 1 ] + w[u];
f[u][0] = 0;
return ;
}
int main()
{
n = read() , m = read();
memset( head , -1 , sizeof( head ) );
for( register int i = 1 , u ; i <= n ; i ++ )
{
u = read() , w[i] = read();
add( u , i );
}
m ++;
dfs(0);
cout << f[0][m] << endl;
return 0;
}
Luogu P1122 最大子树和
这道题的思路类似最大子段和,只不过是在树上做而已
题目给的是以棵无根树,但在这道题没有什么影响
我们以\(1\)来做整棵树的根节点,然后\(f[i]\)表示以\(i\)为根的子树的最大子树和
每次递归操作,先计算出子树的权值在贪心的选择即可
#include <bits/stdc++.h>
using namespace std;
const int N = 16005 , INF = 0x7f7f7f7f;
int n , v[N] , head[N] ,f[N] , ans;
vector<int> e[N];
bool vis[N];
inline int read()
{
register int x = 0 , f = 1;
register char ch = getchar();
while( ch < '0' || ch > '9' )
{
if( ch == '-' ) f = -1;
ch = getchar();
}
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x * f;
}
inline void dfs( int x )
{
f[x] = v[x] , vis[x] = 1;
for( register auto it : e[x] )
{
if( vis[it] ) continue;
dfs( it );
f[x] = max( f[x] , f[x] + f[it] );
}
return ;
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; v[i] = read() , i ++ );
for( register int i = 1 , u , v ; i < n ; i ++ )
{
u = read() , v = read();
e[u].push_back(v) , e[v].push_back(u);
}
dfs( 1 );
ans = -INF;
for( register int i = 1 ; i <= n ; i ++ ) ans = max( ans , f[i] );
cout << ans << endl;
return 0;
}
Luogu P2016 战略游戏
f[i][0/1]表示节点i选或不选所需要的最小代价
如果当前的节点选,子节点选不选都可以
如果当前节点不选,每一个子节点都必须选,不然无法保证每条边都被点亮
递归计算子节点在根据这个原则进行转移即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1505;
int n , f[N][2];
bool vis[N];
vector< int >e[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline void dfs( int x , int fa )
{
vis[x] = 1;
f[x][1] = 1;
for( register auto it : e[x] )
{
if( it == fa ) continue;
dfs( it , x );
f[x][0] += f[it][1];
f[x][1] += min( f[it][1] , f[it][0] );
}
return ;
}
int main()
{
n = read();
for( register int i = 1 , u , v , k ; i <= n ; i ++ )
{
for( u = read() + 1 , k = read() ; k >= 1 ; v = read() + 1 , e[u].push_back(v) , e[v].push_back(u) , k -- );
}
dfs( 1 , -1 );
cout << min( f[1][0] , f[1][1] ) << endl;
return 0;
}
Luogu P2458 保安站岗
这道题和上一道比较类似,但这道题他是要覆盖每一个点而不是每一个边
设f[i][0/1/2]表示第i个点被自己、儿子、父亲所覆盖且子树被全部覆盖,所需要的最小代价
如果被自己覆盖,则儿子的状态可以是任意状态,所以f[u][0] += min( f[v][0] , f[v][1] , f[v][2] )
如果被父亲覆盖,则儿子必须被自己或孙子覆盖,所以f[u][2] += min( f[v][0] , f[v][1] )
如果被儿子覆盖,只需有一个儿子覆盖即可,其他的随意,所以f[u][1] += min( f[v][0] , f[v][1] )
但要特判一下如果所有的儿子都是被孙子覆盖比自己覆盖自己更优的话
为了保证合法就要加上min( f[v][0] - f[v][1])
递归操作根据上述规则转移即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1500 , INF = 1e9+7;
int n , w[N] , f[N][3];
//f[i][0/1/2] 分别表示 i 被 自己/儿子/父亲 覆盖
vector< int > e[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x ;
}
inline void add( int u , int v )
{
e[u].push_back( v ) , e[v].push_back( u );
}
inline void dfs( int x , int fa )
{
f[x][0] = w[x];
register int cur , cnt = INF;
register bool flag = 0;
for( auto it : e[x] )
{
if( it == fa ) continue;
dfs( it , x );
cur = min( f[it][0] , f[it][1] );
f[x][0] += min( cur , f[it][2] );//当前点已经选择,儿子无所谓
f[x][2] += cur; // 当前点被父亲覆盖,儿子必须覆盖自己或被孙子覆盖
if( f[it][0] < f[it][1] || flag ) flag = 1;// 如果有选择一个儿子,比儿子被孙子覆更优,做标记
else cnt = min( cnt , f[it][0] - f[it][1] );
f[x][1] += cur;
}
if( ! flag ) f[x][1] += cnt;//如果全部都选择儿子被孙子覆盖,则强制保证合法
}
int main()
{
n = read();
for( register int i = 1 , u , k , v ; i <= n ; i ++ )
{
u = read() , w[u] = read() , k = read();
for( ; k >= 1 ; v = read() , add( u , v ) , k -- );
}
dfs( 1 , 0 );
cout << min( f[1][0] , f[1][1] ) << endl;
return 0;
}
Luogu P1273 有线电视网
树上分组背包,其实做起来的过程类似普通的分组背包
f[i][j]表示对于节点i,满足前j个儿子的最大权值
然后就是枚举一下转移就好
#include<bits/stdc++.h>
#define PII pair< int , int >
#define v first
#define w second
using namespace std;
const int N = 3005 , INF = 0x7f7f7f7f;
int n , m , pay[N] , f[N][N];
vector< PII > e[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline int dfs( int x )
{
if( x > n - m )
{
f[x][1] = pay[x];
return 1;
}
register int sum = 0 , t ;// sum 当前节点子树有多少个点 , t 子节点子树有多少点
for( register auto it : e[x] )
{
t = dfs( it.v ) , sum += t;
for( register int j = sum ; j > 0 ; j -- )
{
for( register int i = 1 ; i <= t && j - i >= 0 ; i ++ )
{
f[x][j] = max( f[x][j] , f[x][ j - i ] + f[ it.v ][i] - it.w );
}
}
}
return sum;
}
int main()
{
n = read() , m = read();
for( register int i = 1 , k , x , y ; i <= n - m ; i ++ )
{
for( k = read() ; k >= 1 ; x = read() , y = read() , e[i].push_back( { x , y } ) , k -- );
}
for( register int i = n - m + 1 ; i <= n ; pay[i] = read() , i ++ );
memset( f , - INF , sizeof( f ) );
for( register int i = 1 ; i <= n ; f[i][0] = 0 , i ++ );
dfs( 1 );
for( register int i = m ; i >= 1 ; i -- )
{
if( f[1][i] < 0 ) continue;
cout << i << endl;
break;
}
return 0;
}
Luogu U93962 Dove 爱旅游
原图是一张黑白染色的图,我们在存权值时\(1\)还是\(1\),\(0\)就当成\(-1\)来存
设\(f[i]\)表示白色减黑色的最大值,\(g[i]\)表示黑色减白色的最大值,递归求解分别转移即可
自己可以看代码理解下
#include<bits/stdc++.h>
#define exmax( a , b , c ) ( a = max( a , max( b , c ) ) )
using namespace std;
const int N = 1e6 + 5;
int n , a[N] , f[N] , g[N] , ans;
vector< int > e[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline void dfs( int u , int fa )
{
g[u] = f[u] = a[u];
for( auto v : e[u] )
{
if( v == fa ) continue;
dfs( v , u );
f[u] += max( 0 , f[v] );
g[u] += min( 0 , g[v] );
}
}
int main()
{
n = read();
for( register int i = 1 ; i <= n ; i ++ ) a[i] = ( !read() ? -1 : 1 );
for( register int i = 1 , x , y ; i < n ; x = read() , y = read() , e[x].push_back(y) , e[y].push_back(x) , i ++ );
dfs( 1 , 0 );
for( register int i = 1 ; i <= n ; exmax( ans , f[i] , -g[i] ) , i ++ );
cout << ans << endl;
return 0;
}
Loj #10153. 二叉苹果树
f[i][j]表示第i个点保留j个节点最大权值
假如左子树保留k个节点,则右子树要保留j-k-1个节点,因为节点i也必须保存
枚举空间k,去最大值即可
#include<bits/stdc++.h>
#define PII pair< int , int >
#define v first
#define w second
using namespace std;
const int N = 105;
int n , m , l[N] , r[N] , f[N][N] , maps[N][N] , a[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline void make_tree( int x )//递归建树
{
for( register int i = 1 ; i <= n ; i ++ )
{
if( maps[x][i] < 0 ) continue;
l[x] = i , a[i] = maps[x][i];
maps[x][i] = maps[i][x] = -1;
make_tree( i );
break;
}
for( register int i = 1 ; i <= n ; i ++ )
{
if( maps[x][i] < 0 ) continue;
r[x] = i , a[i] = maps[x][i];
maps[x][i] = maps[i][x] = -1;
make_tree( i );
break;
}
return ;
}
inline int dfs( int i , int j )
{
if( j == 0 ) return 0;
if( l[i] == 0 && r[i] == 0 ) return a[i];
if( f[i][j] > 0 ) return f[i][j];
for( register int k = 0 ; k < j ; f[i][j] = max( f[i][j] , dfs( l[i] , k ) + dfs( r[i] , j - k - 1 ) + a[i] ) , k ++ );
return f[i][j];
}
int main()
{
n = read() , m = read() + 1;
memset( maps , -1 , sizeof( maps ) );
for( register int i = 1 , x , y ; i < n ; x = read() , y = read() , maps[x][y] = maps[y][x] = read() , i ++ );
make_tree( 1 );
cout << dfs( 1 , m ) << endl;
return 0;
}
二次扫描与换根法
给一个无根树,要求以没个节点为根统计一些信息,朴素的做法是对每个点做一次树形\(DP\)
但是我们发现每次操作都会重复的统计一些信息,造成了时间的浪费,为了防止浪费我们可以用二次扫描和换根法来解决这个问题
- 第一次扫描,任选一个点为根,在有根树上执行一次树形\(DP\),在回溯时进行自底向上的转移
- 第二次扫描,还从刚才的根节点开始,对整棵树进行一次深度优先遍历,在每次遍历前进行自顶向下的推导,计算出换根的结果
AcWing 287. 积蓄程度
g[i]表示以\(1\)为根节点,i的子树中的最大水流,显然这个可以通过树形\(DP\)求出
f[i]表示以\(i\)为根节点,整颗树的最大水流,显然f[1]=g[1]
我们考虑如何递归的求出所以的f[i],在求解这个过程是至顶向下推导的,自然对于任意点i,在求解之前一定知道了f[fa]
根据求g[fa]的过程我们可以知道,\(fa\)出了当前子树剩下的水流是f[fa] - min( d[i] , c[i] )
所以当前节点的最大水流就是d[i] + min( f[fa] - min( d[i] , c[i] ) , c[i] )
按照这个不断的转移即可,记得处理边界,也就是叶子节点的情况
#include<bits/stdc++.h>
#define PII pair< int , int >
#define v first
#define w second
using namespace std;
const int N = 2e5 + 5;
int n , ans , deg[N] , f[N] , d[N];
vector<PII> e[N];
bool vis[N];
inline int read()
{
register int x = 0;
register char ch = getchar();
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' )
{
x = ( x << 3 ) + ( x << 1 ) + ch - '0';
ch = getchar();
}
return x;
}
inline void add( int x , int y , int z )
{
e[x].push_back( { y , z } ) , e[y].push_back( { x , z } );
deg[x] ++ , deg[y] ++;
}
inline void dp( int x )
{
vis[x] = 1 , d[x] = 0;
for( register auto it : e[x] )
{
if( vis[it.v] ) continue;
dp( it.v );
if( deg[it.v] == 1 ) d[x] += it.w;
else d[x] += min( d[it.v] , it.w );
}
return ;
}
inline void dfs( int x )
{
vis[x] = 1;
for( register auto it : e[x] )
{
if( vis[ it.v ] ) continue;
if( deg[ it.v ] == 1 ) f[ it.v ] += d[ it.v ] + it.w;
else f[ it.v ] = d[ it.v ] + min( f[x] - min( d[ it.v ] , it.w ) , it.w );
dfs( it.v );
}
return ;
}
inline void work()
{
for( register int i = 1 ; i <= n ; i ++ ) e[i].clear();
memset( deg , 0 , sizeof( deg ) );
n = read();
for( register int i = 1 , x , y , z ; i < n ; x = read() , y = read() , z = read() , add( x , y , z ) , i ++ );
memset( vis , 0 , sizeof( vis ) );
dp( 1 );
f[1] = d[1];
memset( vis , 0 , sizeof( vis ) );
dfs( 1 );
for( register int i = 1 ; i <= n ; i ++ ) ans = max( ans , f[i] );
printf( "%d\n" , ans );
return ;
}
int main()
{
for( register int T = read() ; T >= 1 ; work() , T -- );
return 0;
}
0x55 状态压缩DP
状压 \(dp\)是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的
本小节会需要一些位运算的知识
Loj #2153. 互不侵犯
强化版?Loj #10170.国王
f[i][j][l]表示第i行状态为j(用二进制表示每一位放或不放)共放了l个国王的方案数
先用搜索预处理出每一种状态,及其所对应的国王数,在枚举状态转移即可
注意要排除相互冲突的状态
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2005 , M = 15;
LL f[M][N][N] , ans;
int num[N] , s[N] , n , k , tot;
inline void dfs( int x , int cnt , int cur )
{
if( cur >= n )
{
s[ ++ tot ] = x;
num[ tot ] = cnt;
return ;
}
dfs( x , cnt , cur + 1 );// cur 不放
dfs( x + ( 1 << cur ) , cnt + 1 , cur + 2 );
// 如果 cur 放则相邻位置不能放
return ;
}
inline void dp()
{
for( register int i = 1 ; i <= tot ; f[1][i][ num[i] ] = 1 , i ++ );
for( register int i = 2 ; i <= n ; i ++ ) // 枚举行
{
for( register int j = 1 ; j <= tot ; j ++ ) // 枚举第 i 行的状态
{
for( register int l = 1 ; l <= tot ; l ++ ) // 枚举 i - 1 行的状态
{
if( ( s[j] & s[l] ) || ( s[j] & ( s[l] << 1 ) || ( s[j] & ( s[l] >> 1 ) ) ) )
continue;//判断冲突情况
for( register int p = num[j] ; p <= k ; p ++ )
f[i][j][p] += f[ i - 1 ][l][ p - num[j] ];//转移
}
}
}
for( register int i = 1 ; i <= tot ; i ++ ) ans += f[n][i][k];
return ;
}
int main()
{
cin >> n >> k;
dfs( 0 , 0 , 0 );
dp();
cout << ans << endl;
}
Loj #10171.牧场的安排
与上一题不同的是不用统计数量了,状态自然就少了一维f[i][j]表示第i行状态为j的方案数
但增加的条件就是有些点不能选择,在预处理的过程中在合法的基础上枚举状态,这样可以在后面做到很大的优化
#include <bits/stdc++.h>
using namespace std;
const int N = 15 , M = 1000 , mod = 1e8 , T = 4196;//2^12 = 4096开大一点
int n , m , ans , f[N][M];
struct state
{
int st[T] , cnt;
}a[N];//对应每行的每个状态,和每行的状态总数
inline void getstate( int x , int t )
{
register int cnt = 0;
for( register int i = 0 ; i < ( 1 << m ) ; i ++ )
{
if( (i & ( i << 1 ) ) || ( i & ( i >> 1 ) ) || ( i & t ) ) continue;//判断冲突情况
a[x].st[ ++ cnt ] = i;
}
a[x].cnt = cnt;
return ;
}
inline void init()
{
cin >> n >> m;
for( register int i = 1 , t = 0 ; i <= n ; t = 0 , i ++ )
{
for( register int j = 1 , x ; j <= m ; j ++ )
{
cin >> x;
t = ( t << 1 ) + 1 - x;
}
//是与原序列相反的 0代表可以 1代表不可以
getstate( i , t );
}
return ;
}
inline void dp()
{
for( register int i = 1 ; i <= a[1].cnt ; f[1][i] = 1 , i ++ );
//预处理第一行
for( register int i = 2 ; i <= n ; i ++ )//枚举行
{
for( register int j = 1 ; j <= a[i].cnt ; j ++ )//枚举第 i 行的状态
{
for( register int l = 1 ; l <= a[ i - 1 ].cnt ; l ++ )//枚举第 i-1 行的状态
{
if( a[i].st[j] & a[ i - 1 ].st[l] ) continue;//冲突
f[i][j] += f[ i - 1 ][l];
}
}
}
for( register int i = 1 ; i <= a[n].cnt ; i ++ )
ans = ( ans + f[n][i] > mod ? ans + f[n][i] - mod : ans + f[n][i] );//用减法代替取模会快很多
return ;
}
int main()
{
init();
dp();
cout << ans % mod << endl;
return 0;
}
Loj #10173.炮兵阵地
这道题的状压过程和上一题很类似,所以处理的过程也很类似
f[i][j][k]表示第i行状态为j,第i-1行状态为k所能容纳最多的炮兵
状态转移与判定合法的过程与上一题基本相同,不过本题n的范围比较大,会MLE,要用滚动数组优化
#include<bits/stdc++.h>
using namespace std;
const int N = 105 , M = 12 , T = 1030;
int n , m , f[3][T][T] , ans;
struct state
{
int cnt , st[N];
}a[N];
inline bool read()
{
register char ch = getchar();
while( ch != 'P' && ch != 'H' ) ch = getchar();
return ch == 'H';
}
inline int get_val( int t )
{
register int res = 0;
while( t )
{
res += t & 1;
t >>= 1;
}
return res;
}
inline void get_state( int x , int t )
{
register int cnt = 0;
for( register int i = 0 ; i < ( 1 << m ) ; i ++ )
{
if( ( i & t ) || ( i & ( i << 1 ) ) || ( i & ( i << 2 ) ) || ( i & ( i >> 1 ) ) || ( i & ( i >> 2 ) ) ) continue;
a[x].st[ ++ cnt ] = i;
}
a[x].cnt = cnt;
return ;
}
int main()
{
cin >> n >> m;
for( register int i = 1 , t = 0 ; i <= n ; t = 0 , i ++ )
{
for( register int j = 1 , op ; j <= m ; j ++ )
{
op = read();
t = ( t << 1 ) + op;
}
get_state( i , t );
}
for( register int i = 1 ; i <= a[1].cnt ; i ++ )
{
for( register int j = 1 ; j <= a[2].cnt ; j ++ )
{
if( a[1].st[i] & a[2].st[j] ) continue;
f[2][j][i] = get_val( a[1].st[i] ) + get_val (a[2].st[j] );
}
}
for( register int i = 3 ; i <= n ; i ++ )
{
for( register int j = 1 ; j <= a[i].cnt ; j ++ )
{
for( register int k = 1 ; k <= a[ i - 1 ].cnt ; k ++ )
{
for( register int l = 1 ; l <= a[ i - 2 ].cnt ; l ++ )
{
if( ( a[i].st[ j ] & a[ i - 1 ].st[ k ] ) || ( a[i].st[j] & a[ i - 2 ].st[l] ) || ( a[ i - 1 ].st[k] & a[ i - 2 ].st[l] ) ) continue;
f[ i % 3 ][j][k] = max( f[ i % 3 ][j][k] , f[ ( i - 1 ) % 3 ][k][l] + get_val( a[i].st[j] ) );
}
}
}
}
for( register int i = 1 ; i <= a[n].cnt ; i ++ )
{
for( register int j = 1 ; j <= a[ n - 1 ].cnt ; j ++ ) ans = max( ans , f[ n % 3 ][i][j] );
}
cout << ans << endl;
return 0;
}