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;   
}
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息