DP

辣雞動態規劃毀我青春~~

動態規劃中的基本概念

狀態:要算什麼算法

轉移方程:如何去算數組

無後效性:把狀態看作一個點,轉移過程看做一條邊,動態規劃的全部概念組成了一個有向無環圖(\(DAG\)),因此在作題時必定要考慮是否是知足無後效性優化

一旦出現亂序的狀況,應該對這個圖先進行一個拓撲排序spa

揹包問題

\(N\)個物品,有一個\(M\)溶劑的包,每一個物品有一個體積和價值,要求最大化價值之和code

\(i\)個物品的價值爲\(V_i\),佔\(W_i\)的空間排序

\(dp[i][j]\)表示已經放好了前\(i\)個物品,如今放進去的物品的體積之和爲\(j\)string

考慮轉移方程:it

\(i+1\)個物品只有兩種狀況:放入揹包與不放入揹包io

若是放入第\(i+1\)個物品,體積不變,價值也不變

若是不放入第\(i+1\)個物品,應該轉移爲\(dp[i+1][j+V_{i+1}]\)

如今對於每個物品就只有放和不放兩種狀況(這是用本身更新別人的方法)

若是用別人更新本身呢?

對於\(dp[i][j]\),若是第\(i-1\)個物品沒有放,是由\(dp[i-1][j]\)轉移而來的

不然就是由\(dp[i-1][i-V_i]\)更新而來的(加上\(W_i\)

代碼以下:

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

int dp[10010][10010];
int w[10010],v[10010];
int n,m;
int ans=0;

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&v[i],&w[i]);
    for(int i=1;i<=n;++i)
        for(int j=0;j<=m;++j)
        {
            dp[i][j]=dp[i-1][j];
            if(j>=v[i])
                dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
        }   
    for(int i=0;i<=m;++i)
        ans=max(ans,dp[n][i]);
    printf("%d",ans);
    return 0;
}

如今每一個物品能夠用無限次,這個時候要怎麼辦呢?

直接枚舉每一個物品用多少次就好了

可是複雜度過高\(\Omega \omega \Omega\)

咱們只須要將原來的\(dp[i-1][j-v_i]+w_i\)變爲\(dp[i][j-v_i]+w_i\)就行了

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

int dp[10010][10010];
int w[10010],v[10010];
int n,m;
int ans=0;

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&v[i],w[i]);
    /*for(int i=1;i<=n;++i)
        for(int j=0;j<=m;++j)
            for(int k=0;k*v[i]<=j;++k) 
            {
                    dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
            }*/ 
    for(int i=1;i<=n;++i)
        for(int j=0;j<=m;++j)
        {
            dp[i][j]=dp[i-1][j];
            if(j>=v[i])
                dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
        }   
    for(int i=0;i<=m;++i)
        ans=max(ans,dp[n][i]);
    printf("%d",ans);
    return 0;
}

那若是每一個物品只能用有限次,那怎麼辦呢

仍是直接枚舉每一個物品用多少次就好了

這個東西的複雜度是\(O(n^3)\)

那麼如何優化呢?(有點難)

有限揹包最慢的地方是枚舉每一個物品用了多少次

思想就是將一個揹包變成多個捆綁包(進行二進制分解,若是不夠了,就只能委屈一下最後一個捆綁包了),而後就變成了一個\(01\)揹包

\(13\)爲例,能夠拆成:\(1\)\(2\)\(4\)\(6\)四個捆綁包

代碼以下(主要是拆捆綁包的部分)

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

int dp[10010][10010];
int w[10010],v[10010];
int n,m;
int ans=0;

int main()
{
    scanf("%d%d",&n,&m);
    int cnt=0; 
    for(int i=1;i<=n;++i)
    {
        int v_,w_,z;
        scanf("%d%d%d",&v_,&w_,&z);
        int x=1;
        while(x<=z)
        {
            cnt++;
            v[cnt]=v_*x;
            w[cnt]=w_*x;
            z-=x;
            x*=2;
        }
        if(z>0)
        {
            cnt++;
            v[cnt]=v_*z;
            w[cnt]=w_*z;
        }
    }
    for(int i=1;i<=cnt;++i)
        for(int j=0;j<=m;++j)
        {
            dp[i][j]=dp[i-1][j];
            if(j>=v[i])
                dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
        }   
    for(int i=0;i<=m;++i)
        ans=max(ans,dp[n][i]);
    printf("%d",ans);
    return 0;
}

基礎\(DP\)

例題一 數字三角形:

狀態:\(dp[i][j]\)表示走到第\(i\)行第\(j\)列時所走的路徑的最大值

狀態轉移方程:\(dp[i][j]=max(dp[i-1][j-1],f[i-1][j])+a[i][j]\)

例題二 數字三角形:

誒誒,怎麼仍是水三角形???

如今求的是答案對\(100\)取模以後的最大值

怎麼作呢?

鍾皓曦:維度不夠加一維,維度不夠再加一維,你總有一天會過的

狀態:布爾狀態,\(dp[i][j][k]\)表示走到第\(i\)行第\(j\)列的數對\(100\)取模等於\(k\)是否是可行的

狀態轉移:考慮用本身更新別人,由\(dp[i][j][k]\)能夠轉移到\(dp[i+1][j][(a[i+1][j]+k)\mod 100]\)\(dp[i+1][j+1][(a[i+1][j+1]+k)\mod 100]\)

代碼以下:

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

bool dp[233][233][233];
int n;
int a[233][233];
int ans;

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=i;++j)
            scanf("%d",&a[i][j]);
    dp[1][1][a[1][1]%100]=true;
    for(int i=1;i<=n;++i)
        for(int j=1;j<=i;++j)
            for(int k=0;k<100;++k)
                if(dp[i][j][k])
                {
                    dp[i+1][j][(k+a[i][j])%100]=true;
                    dp[i+1][j+1][(k+a[i+1][j+1])%100]=true;
                }
    for(int i=1;i<=n;++i)
        for(int j=0;j<100;++j)
            if(dp[n][i][j]) ans=max(ans,j);
    printf("%d",ans);
    return 0; 
}

例題三 最長上升子序列:

狀態:\(dp[i]\)表示以\(i\)結尾的最長上升子序列的長度

狀態轉移:\(f[i]=max(f[j])+1\)知足\(1\leq j<i\)而且\(a[j]<a[i]\)

這個算法複雜度是\(O(n^2)\)

若是數據再大一點,就能夠用線段樹

區間\(DP\)

例題一 合併石子:

把兩對相鄰的石子合併爲一堆石子,每次合併的代價就是兩堆石子的個數之和,如今問把\(n\)堆石子合併爲\(1\)堆石子的最小代價是多少

咱們能夠發現每次合併是將一段區間的石子合併

這就是一個區間\(DP\)

區間\(DP\)的狀態通常爲\(dp[l][r]\)

狀態:$dp[l][r] \(表示\)[l,r]\(區間合併的最小代價(\)dp[l][l]=0$)

每次均可以在區間中找到一個分界線,最後合併分界線兩邊的區間

那麼就能夠枚舉分界線\(p\)

狀態轉移:\(dp[l][r]=min(dp[l][r],dp[l][p]+dp[p+1][r]+sum[r]-sum[l-1]\)

例題二 無題

把一排矩陣排排坐,保證矩陣是能夠相乘獲得一個結果的,如今在這幾個矩陣中加上括號,使得運算次數最小

狀態:\(dp[l][r]\)表示將第\(l\)個矩陣和第\(r\)個矩陣的最小運算次數

狀態轉移:\(dp[l][r]=min(dp[l][p]+f[p+1][r]+a[l]\times a[p+1]\times a[r+1])\)\(p\)是中點。

狀壓\(DP\)

例題一 旅行商問題

在平面上由\(n\)個點,給出每一個點的座標,如今由\(1\)號點出發,把全部點都走一遍而後回到\(1\)號點,求最短距離

首先,通常來講咱們沒有必要把一個點走兩次

當前在哪一個點、走過了那個點是兩個變化的量

狀態:\(dp[s][i]\)\(i\)表示如今走到了第\(i\)個點,\(s\)表示走過了那些點

理論上來講,\(s\)是要用一個數組來維護的,可是咱們如今要用一個數來表示

那麼咱們就能夠用一個二進制數來表示哪些點沒走,哪些點走了(\(1\)表示走過,\(0\)表示沒走過)

狀壓\(DP\)能解決的範圍在\(n\leq 20-22\),由於複雜度爲\(O(2^n\times n^2)\)

可愛的代碼:

#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring> 

using namespace std;

double dp[233][233];
double x[233],y[233];
double ans=0x3f;
int n;

double dis(int xx,int yy)
{
    return sqrt((x[xx]-x[yy])*(x[xx]-y[yy])+(y[xx]-y[yy])*(y[xx]-y[yy]));
}

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;++i)
        scanf("%lf%lf",&x[i],&y[i]);
    memset(dp,0x3f,sizeof(dp));
    dp[1][0]=0;
    for(int s=0;s<(1<<n);s++)
        for(int i=0;i<n;++i)
            if(dp[s][i]<0x3f)//這是一個可行的方案 
            {
                for(int j=0;j<n;++j)
                    if((s>>j)&1==0)//將s二進制的第j爲取了出來 
                    {
                        int news=s|(1<<j);//把s的第j爲變成了1
                        dp[news][j]=min(dp[news][j],dp[s][i]+dis(i,j)); 
                    }
            } 
    for(int i=0;i<n;++i)
        ans=min(ans,dp[(1<<n)-1][i]+dis(i,0));
    printf("%d",ans);
    return 0;
}

例題二 玉米田

\(JOHN\)要在一片牧場上種草,每塊草坪之間沒有相鄰的邊

狀態:\(dp[i][s]\)表示前\(i\)行的草都種完了,\(s\)表示第\(i\)行的草種成了什麼樣,這種狀況下的方案數爲\(dp[i][s]\)

考慮第\(i+1\)行如何種草:

首先第\(i+1\)行種的草沒有兩個連續的

其次第\(i\)行的草和第\(i+1\)行的草沒有相鄰的草,就是\(s\)&\(s\)'=\(0\)

例題三 \(K\)國王問題

\(N\times N\)的棋盤中,放\(k\)個國王,使這些國王不能相互攻擊到對方(國王的攻擊範圍就是其周圍的\(8\)個格子)

參照上一道題(種國王)

狀態:\(dp[i][s]\)表示前\(i\)行的國王都種完了,\(s\)表示第\(i\)行的國王種成了什麼樣,這種狀況下的方案數爲\(dp[i][s]\)

可是這個題要多放一個國王,因此咱們要多加一個維度

新狀態:\(dp[i][s][j]\)表示前\(i\)行的國王都種完了,\(s\)表示第\(i\)行的國王種成了什麼樣,如今放了\(j\)個國王,這種狀況下的方案數爲\(dp[i][s]\)

數位\(DP\)

是在\(DP\)的過程當中按照數的位數進行轉移的

例題一 無題

給出兩個數\(l、r\),求這之間有多少個數

首先,\([l,r]\)之間有多少數,就是求\([0,l]\)\([0,r]\)之間分別有多少數,而後相減

數位\(DP\),就是將一個\(n\)位數,抽象爲\(n\)個格子,若是要求有多少個數小於這個數,能夠用數字填滿這\(n\)個格子

注意:必定要從高位向低位一位一位的填

狀態:\(dp[i][j]\)中,已經填好了前\(i\)位,\(j=0\)表示當前這個數必定小於\(x\)\(j=1\)表示當前這個數不肯定是否小於\(x\)

狀態轉移:數位\(DP\)的轉移都是去枚舉下一位填什麼數

代碼:

#include<cstdio>
#include<iostream>
#include<cstring>

using namespace std;

int l,r;
int dp[10010][10010];
int z[10010];

int solve(int x)
{
    int l=0;
    while(x>0)
    {
        l++;
        z[l]=x%10;
        x/=10;
    }
    memset(dp,0,sizeof(dp));
    dp[l+1][1]=1;//在第l+1位以後都只能填0
    /*轉移考慮用本身去更新別人*/
    for(int i=l+1;i>=2;i--)
        for(int j=0;j<=1;++j)
            for(int k=0;k<=9;++k)//枚舉應該填哪個數
            {
                if(j==1&&k>z[i-1])  continue;
                int j_;
                if(j==0)    
                    j_=0;
                else if(k==z[i-1])
                    j_=1;
                else 
                    j_=0;
                dp[i-1][j_]+=dp[i][j];
            }
    return dp[1][0]+dp[1][1];
}

int main()
{
    scanf("%d%d",&l,&r);
    printf("%d",solve(r)-solve(l-1));
    return 0;
}

例題二 無題

\([l,r]\)中全部數的數位之和

一樣,咱們能夠轉化爲\([0,r]\)\([0,l-1]\)之間有的數位之和

狀態:\(dp[i][j]\)表示第\(i\)位已經填完了,\(j=0\)表示當前這個數必定小於\(x\)\(j=1\)表示當前這個數不肯定是否小於\(x\),這時的方案數

例題三 無題

\([l,r]\)中有多少個相鄰兩位數字之差大於等於\(2\)的數

狀態:$ dp[i][j][k]\(表示當前已經填了\)i\(個數,\)j=0\(表示當前這個數必定小於\)x\(,\)j=1\(表示當前這個數不肯定是否小於\)x\(,第\)i\(位填的數是\)k$

例題四 無題

\([l,r]\)中知足各位數字之積位\(k\)的數有多少個

狀態:\(dp[i][j][r]\)表示當前已經填了\(i\)個數,\(j=0\)表示當前這個數必定小於\(x\)\(j=1\)表示當前這個數不肯定是否小於\(x\),當前乘積爲\(r\)

有些\(r\)是永遠不會用到的,就是那些大於\(10\)的質數的倍數

新狀態:\(dp[i][j][a][b][c][d]\)表示當前已經填了\(i\)個數,\(j=0\)表示當前這個數必定小於\(x\)\(j=1\)表示當前這個數不肯定是否小於\(x\),如今的乘積爲\(2^a+3^b+5^c+7^d\)

樹形\(DP\)

例題一 無題

如今給你一棵\(n\)個點的樹,問這個樹有多少點???

樹形\(DP\)通常是\(DP\)以這個點爲根的子樹的狀態

狀態:\(f[i]\)表示以\(i\)爲根的樹有多少個點

狀態轉移:\(f[i]=\sum_{j\in}\)

例題二 無題

給出一棵樹,求出樹的直徑

將兩個點的路徑看作由\(LCA\)向下的兩條路徑

狀態:\(f[i][0]\)表示第\(i\)個點向下的最大值,\(f[i][1]\)表示最小值

狀態轉移:\(f[i][0]=max(f[p_j][0])+1\)\(f[i][1]\)的值應該是剩下的全部兒子的最長路中的最長路

#include<cstdio>
#include<iostream>

using namespace std;

void dfs(int i)
{
    for(p is i's son)//教你們如何寫僞代碼,你只須要boomboomboom,而後boomboomboom,就能boomboom了
        dfs(p);
    for(p is i's son)
    {
        int v=f[p][0];
        if(v>f[i][0])
        {
            f[i][1]=f[i][0];
            f[i][0]=v;
        }
        else if(v>f[i][1])
            f[i][1]=v;
    }
}

int main()
{
    scanf("%d",&n);
    du ru jian shu;
    dfs(1);//強行令1號點爲根 
    return 0;
}

例題三 無題

求全部點之間的路徑之和爲多少

狀態:\(dp[i]\)表示以\(i\)爲根的子樹有多少個點

例題四 無題

1563352056821

狀態:\(dp[i][0/1]\)表示到了第\(i\)個點,\(1\)表示選了改點,\(0\)表示沒有選

若是有一個點選了,那麼這個點的全部兒子都不能選

即:\(dp[i][1]=\sum_{j\in son_i}dp[j][0]+a[i]\)

\(dp[i][0]=\sum_{j\in son_i}max(dp[j][0],dp[j][1])\)

例題五 無題

每一個士兵能夠守護全部與該結點直接相鄰的邊,請問在全部邊都被守護的條件下,最少要安排多少士兵?

狀態:\(dp[i][0/1]\)表示以\(i\)爲根的子樹的全部點都被守護,\(1\)表示有士兵,\(0\)表示沒有,此時的最少士兵

\(f[i][0]=\sum_{j\in son_i}dp[i][1]\)

\(dp[i][1]=\sum_{j\in son_i}max()\)

相關文章
相關標籤/搜索