一些優秀的dp狀態總結

該博客會一直更新優秀dp的狀態

1.求兩個字符串相同子序列的個數:

設dp[i][k] 表示表示字符串s前i個字符與字符串t前k個字符相同的子序列個數

那麼

dp[i][k]=dp[i-1][k]+dp[i][k-1]-dp[i-1][k-1]//類似於容斥

如果s[i]==t[k] dp[i][k]+=dp[i-1][k-1]+1 //新增子序列+自己


2.二進制給出L,求滿足a+b<=L&&a^b=a+b的(a,b)有多少個

首先,a^b=a+b 可以推出 a與b的二進制任何一位都不可以同時爲1

對於每一位來言,ab在這一位上的組合有3種 01 10 00

我們設dp[i][1]代表到第i位,a+b=L且a+b=a^b的對數

       設dp[i][0]代表到第i位,a+b<L且a+b=a^b的對數

那麼如果L的第i位爲1 那麼此時這一位可以取三種情況00 01 10

並且由於權值從大到小枚舉,所以加上1,也不會使得之前小於L的數量改變(因爲小於L 絕對是小於了一個2^k)

所以dp[i][0]=dp[i-1][0]*3  

如果當前 L第i位 爲1 :

dp[i][1]=dp[i-1][1]*2   //取(01 10)使得之前等於L 現在也等於L

dp[i][0]+=dp[i-1][1] // 這一位如果不取可以使之前等於L的變的小於L

如果當前 L第i位 爲0 不做修改


3.將一段遞增的區間劃分爲若干段,要求每段長度必須大於k,求如何劃分使得 每段的極差和最小

劃分dp的題目,比賽中由沒有推出來,賽後自己推出來了,記錄一下。

設f[i]表示前i個元素已經滿足要求的最小花費

首先,區間長度必須要大於k,所以起始取值爲k,又發現在k到2*k-1之間,只能劃分爲一段,否則前面的一段將長度<k

並且 k~2*k-1之間,f[i]的值爲 num[i]-num[1]

對於大於>=2*k的位置,f[i]=min(f[j-1]+num[i]-num[j],f[i])//確保j-1段滿足要求,使得[j,i]段取值成爲新的一段序列,可得出j的取值範圍  (k<=j<=i-k+1)

對轉移方程  :f[i]=min(f[j-1]+num[i]-num[j],f[i])進行轉換:f[i]=min((f[j-1]-num[j])+num[i],f[i])

所以我們只需要維護一個f[j-1]-num[j]的前綴最小值即可,O(1)轉移


4.樹形dp:在樹上的第i個節點放置一個守衛需要花費val[i],放置完成後,相鄰的所有點都被監視到,問如何在樹上放置點,使得花費最小並且監視到所有點?

很不錯的樹形dp,狀態比較難想 ,搞清楚狀態之後其實這題不難

對於dp[i][3],令:

dp[i][0]爲該位置不放節點,該節點由父節點監視。

dp[i][1]爲該位置不放節點,該節點由子節點監視。

dp[i][2]爲該位置放節點

那麼很容易得到:

dp[i][0]:因爲該位置不放節點,由父節點監視,所以子節點有兩種狀態:第一種放置守衛,第二種不放守衛繼續由子節點的子節點監視。

dp[i][1]:因爲該位置不放節點,由子節點監視,所以需要有一個子節點進行監視,然後其餘的子節點兩種狀態,不放-由子節點監視(這時不可以由父節點監視)或者放。進行狀態轉移時,我們首先求和所有子節點的 sum+=min(dp[e][2],dp[e][1]),所以我們枚舉一下放哪一個子節點就好了,枚舉第k個節點監視該節點產生的最小花費爲 sum-min(dp[k][1],dp[k][2])+d[k][2]

dp[i][2]:該位置放,那麼對於子節點的狀態而言,沒有什麼限制,可以加三種狀態的最小值。


5.有n個數字,問從n個數字中取出若干個數字,他們的和modp爲m,問有多少種取法?

這題是基礎計數dp,很容易想到對於每一個數字來討論,dp[k]代表mod p爲k的取法有多少種,剩下的無法文字解釋,然後這篇文章的第一個代碼就出現了:

    dp[num[1]%p]=1;
    for(int i=2;i<=n;i++){
        for(int k=0;k<p;k++) cop[k]=dp[k];
        for(int k=0;k<p;k++)
            cop[k]=dp[k]+dp[(k-num[i]%p+p)%p];
        for(int k=0;k<p;k++)
            dp[k]=cop[k];
        dp[num[i]%p]++;
    }
    printf("%lld\n",m==0?dp[m]+1:dp[m]);

轉移顯然成立,每一位的貢獻都計算在內了。


6.給定一個集合A,集合A的真子集共有2^n-1個,定義每個集合的權值是集合內所有數之和,問真子集權值排序之後中位數是多少?

題目要求 :N<=2000,a[i]<=2000

首先考慮如何與dp靠邊,假設我選擇一個集合權值爲S,那麼他的補集就是 SUM-S(SUM爲全集的權值)

所以說我們將空集考慮進去,那麼 所有的權值都是關於S/2對稱的,也就是說中位數必須是(SUM+1)/2到SUM的其中一個。

那麼dp的思路也就來,設dp[i][j]代表前i個數是否可以組成數j:

狀態轉移方程即爲 :dp[i][j]|=dp[i-1][j-num[i]]

考慮到第二維的狀態過於龐大所以採用bitset優化爲一維設f[i]代表數字i可以表示那麼,對於新輸入的數字x,則有f|=f<<x

最後統計(SUM+1)/2~SUM間第一個可以表示的數即可

bitset轉義複雜度是普通複雜度的64倍,即該複雜度爲:O(\frac{n*SUM}{64})~=7e7


7.天平秤重:給定一些物品既可以放在左邊也可以放在右邊,問是否可以稱出重爲w的物品?

揹包問題:

假設當前k可以稱出,那麼abs(k-x)與(k+x)都可以稱出,那麼狀態轉移方程很顯然了。

不過此時只能用二維了,因爲與k-x與k+x都有關。

所以二維轉移

空間複雜度:O(N*M) M爲所有物品最大總重量

時間複雜度:O(N*M) M爲所有物品最大總重量


8.數位dp:1~n中有多少個數可以被自己數位的和整除?

顯然成立問題:

首先考慮,模數一共有9*12種,分開枚舉即可

dp[pos][sum][remain]表示到達第pos位時,sum爲數位和,remain爲前面所表示的數對當前模數取餘剩餘多少與當前sum加和爲多少,確定兩個狀態即可加上滿子樹的值

9.一個長度爲n的字符串,有多少個本質不同的長度爲k的子序列?

好題!

考慮對於第i位而言,他可以作爲長度爲k的子序列中的第[1,k]位

所以由此可以推出dp[i][k]表示以str[i]結尾的長度爲k的字符串的數量

很容易得到方程:

dp[i][k]=\sum _{j=1}^{j=i-1}dp[j][k-1]

但是這樣計算的子序列中顯然存在重複,並且時間複雜度也不允許。

所以考慮什麼情況下不重複:

考慮當前str[i]作爲第j位,那麼第j-1位置上的字母一共有26種選擇,所以此時狀態方案數只需要加上這26種即可,但是前面有很多相同字母,該加哪個字母的方案數呢?

例如:abbd  以d結尾的方案數應該加第二個b還是第一個b,答案顯然第二個b。

所以狀態轉移就很清楚了:

dp[i][k]=\sum _{j=0}^{j=i-1}dp[j][k-1] \{j>=a&&j<=z}

所以可以維護一個數組f[j]{j>=1&&j<=26},表示在i之前離i最近的j+'a'的值是多少。

之後,dp[i][k]+=f[j]——{1<=j<=26}

以上是第一種解法:

考慮第二種解法:

原理不變,第i個位置長度爲k的方案數只與離他最近的有關係——由此可以用序列自動機優化一下:

dp[nex[i][c]][k+1]+=dp[i][k]

因爲只要出現兩個相同的字母,前一個字母已經把後一個字母的方案數給隔斷了,所以顯然沒有重複

///bacc -> ba bc ac cc

總結:子序列問題摻雜dp可以與序列自動機結合一下

10.擺花問題:有n種花編號從1到n,有m個花壇,每個花壇一朵花,每種花不得超過a_i個,且必須按照編號從小到大順序排列,問有多少種組合方式?n<=100,a_i<=100

樣例:

2 4

3 2

ans:2 

解釋 : (1,1,1,2) ,(1,1,2,2)

思路轉換一下,剛開始想麻煩了,可以由題目限制可以得到,這些畫必須從編號從小到大放,而且如果放了k個,那麼這k個一定是連續的。

所以可以考慮三重for:

現在開始放編號爲i的花

從第j個位置開始往前放k個

這樣放就保證了,絕對從編號有小到大開始放

所以設dp[i][k]爲第i個位置,放完標號爲k的花之後一共會有多少種方案:

Code

    for(int i=1;i<=n;i++){///放第幾個標號
        for(int k=1;k<=a[i];k++){///一次放多少個
            for(int j=k;j<=m;j++){
                dp[i][j]=(dp[i][j]+dp[i-1][j-k])%mod;
            }
        }
        for(int j=1;j<=m;j++) dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;///更新 因爲覆蓋之前的第j個位置可以由任意編號結束
    }

 解決,需要注意的是,這裏的dp只能二維 ,不然會影響轉移。

11.樹形dp:給出一個棵樹,求對於每條邊而言,必須經過這條邊的最長鏈多長?

最長鏈 首先考慮樹的直徑求法,通常轉換一下:邊數 + 1 = 點數  所以記錄點數即可

這裏用到了換根dp:

對於每個邊而言,兩端點爲u、e,分爲向上(從u出發)的和向下(從e出發)的兩條路徑,必須經過這條邊的最長鏈的長度即爲 向上的最大+向下的最大

首先處理向下的:

dp[u] = max(dp[u],dp[e]+1)

並且顯然從e出發的最大值即爲dp[e]

再處理向上的:

首先記錄u的父節點fa,那麼父節點向上的最大肯定也是u節點向上最大的一種選擇,所以首先:

dp1[u]=dp1[fa]+1(加上當前節點u)

其次我們遍歷所有u的所有兄弟節點,此時u的向上節點長度: dp1[u] = dp1[bro]+2  (fa節點與u節點)

但是u不一定要從fa過來,還有可能從u的其他孩子節點過來,所以爲了保證更新是最大值,記錄u向下的一個最大值,一個次大值。

當枚舉這條邊的終點e滿足dp[e] +1 ==dp[u]時(i代表當前邊):

ans[i] = max(ans[i],次大值+dp[e])

ans[i]=max(ans[i],dp[e]+dp1[u])

否則:

ans[i] = max(ans[i],最大值+dp[e])

ans[i]=max(ans[i],dp[e]+dp1[u])


12.給出一個矩陣,每次操作可以矩陣內的某個數-1,每個數可以可以向右向下移動,問操作幾次可以使得從(1,1)到(n,m)存在一個單調自增1的路徑

經典套路:枚舉不動點,不動點確定,起點就確定,終點也確定,進行dp檢驗即可

複雜度O(n^2*m^2)


13.給出一段只含有小寫字母的字符串,詢問最少修改多少個字母使得字符串中不含有aabb

令:

dp[1] 代表 字符串中不含有a的最小修改次數

dp[2] 代表 字符串中不含有aa的最小修改次數

dp[3] 代表 字符串中不含有aab的最小修改次數

dp[4] 代表 字符串中不含有aabb的最小修改次數

所以如果當前字符爲'a',那麼dp[2] = min(dp[1],dp[2]+1) :

可以修改前i-1個a:dp[1]

修改當前字符a:dp[2]+1

這裏dp[2]+1可能會存在一些疑惑,這裏解釋一下:

dp[2]來源於兩種情況:

1.之前有aa存在,修改掉了a,還剩一個a

2.之前有a存在,不做任何修改。

此時再碰到一個a時 ,必定會出現aa,所以此時dp[2]需要修改一下才可以不會出現aa。

所以這樣寫保證dp[2]代表了再遇到一個a時 序列中百分百含有aa 所以此時需要修改當前的a ,或者 使得序列沒有第一個a

至於dp[3] dp[4]同理

14.給定一棵樹,每個點都有權值,刪除某個點需要花費其權值,求出最少花費使得樹上沒有長度大於等於l的鏈。

設dp[i][k]表示以i爲端點的最長鏈小於等於k 並且子樹都滿足題意

所以:

1.dp[i][0] 代表的即爲 刪除該節點 ,但又要保證子樹滿足題意,所以說對於任意一顆子樹,都要加上最小值:

dp[i][0] = num[i] + \sum_{e=son(i)} min(dp[e][k]) ,0<=k<=m-1

2.dp[i][k] ,k>0 時代表保留該節點。

保留該節點,對於新來的子樹,枚舉新來子樹的最長鏈與當前確定的最長鏈,只要i+j<m即可轉移:

        for(int i=1;i<m;i++){
            for(int k=0;k<m;k++){
                if(i+k<m) dp[u][max(i,k+1)]=min(dp[u][max(i,k+1)],temp[i]+dp[e][k]);
                else break;
            }
        }

此時記得max(i,k+1)

temp[i] 保留上一層的值

此時複雜度爲O(n^2)->這麼考慮:每兩個點都只會被兩點的lca計算一次!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章