0x50 动态规划

0x51 线性DP

LIS

最长上升子序列,给定一个长度为\(N​\)序列\(A​\),求出数值单调递增的子序列长度最长是多少c++

\(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\)的长度spa

输出路径

\(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 递增

最长上升子序列问题的模板题,求出当前序列的最长上升子序列长度code

#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_boundblog

AcWing 271. 杨老师的照相排列

这道题的题面很是的绕排序

其实就是让你放\(1\cdots N\)个数,每一行,每一列都是递增的

显然放的顺序是没有影响的,那咱们不妨从\(1\)\(N\)逐个放

首先先找一些性质

  1. \(1\)必定放在\((1,1)\)上,否则不能知足递增

  2. 咱们在放每一行的时候,必需要从左到右挨个放,显然在放\(x\)的时候,若是\(x-1\)没有放,那么在之后放\(x-1\)这个位置的数必定会比\(x\)更大

  3. 咱们在放\((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]表示长度为iB[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]以前的状态因此只保存两个状态便可

咱们用两个值tonow来表示状态的下标,每次转移后交换两个值便可,这样能够减小大部分的空间

#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]为将lr所有涂成目标状态的最下代价

显然当l == r时,f[l][r] = 1

l != ropt[l] == opt[r]时,只需在涂抹中间区间时多涂抹一格,即f[l][r] = min( f[l+1][r] , f[l][r-1 ] )

l != ropt[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]表示站成目标队列lr部分,且最后一我的在左或右边的方案数

根据题目的要求稍做思考就能获得转移方程

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. 二叉苹果树

Luogu P2015 二叉苹果树

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\)

可是咱们发现每次操做都会重复的统计一些信息,形成了时间的浪费,为了防止浪费咱们能够用二次扫描和换根法来解决这个问题

  1. 第一次扫描,任选一个点为根,在有根树上执行一次树形\(DP\),在回溯时进行自底向上的转移
  2. 第二次扫描,还从刚才的根节点开始,对整棵树进行一次深度优先遍历,在每次遍历前进行自顶向下的推导,计算出换根的结果

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;   
}
相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息