1. 程式人生 > 實用技巧 >講義 GDFZOJ 【36、37】 動態規劃基礎1、動態規劃基礎2

講義 GDFZOJ 【36、37】 動態規劃基礎1、動態規劃基礎2

動態規劃基礎1

【傳送門】

T1 滑雪

難度:普及-, 做法:記憶化搜尋

首先for一遍起點,分別使用dfs搜尋,搜尋過程記錄答案,大大減少複雜度。

Code:

#include<bits/stdc++.h>
using namespace std;
int r,c,maxx=0,mx,my,dx[5]={-1,0,1,0},dy[5]={0,1,0,-1};
int a[110][110],dp[110][110],ans;
int dfs(int x,int y)
{
    if(dp[x][y])	return dp[x][y];
    int r=0;
    for(int i=0;i<4;i++)
    {
        int xx=x+dx[i],yy=y+dy[i];
        if(a[xx][yy]<a[x][y])
            r=max(r,dfs(xx,yy));
    }
    dp[x][y]=r+1;
    return ________;
}
int main()
{
    scanf("%d%d",&r,&c);
    memset(a,0x3f,sizeof(dp));
    for(int i=1;i<=r;i++)	
        for(int j=1;j<=c;j++)
        {
            scanf("%d",&a[i][j]);
            if(a[i][j]>maxx)
                mx=i,my=j,maxx=a[i][j];
        }
    for(int i=1;i<=r;i++)
        for(int j=1;j<=c;j++)
            ans=max(ans,dfs(i,j));
    cout<<ans;
}

T2 最長下降子序列

難度:普及-, 做法:動態規劃

\(dp_{i}\) 為以 \(a_i\) 結尾的最長上升子序列的長度,不難得出狀態轉移方程為 \(dp_j=max(dp_j,\ dp_i+1)\ (1 \leq i \leq n,\ 1 \leq j < i,\ a_i<a_j)\)

Code:

#include<bits/stdc++.h>
using namespace std;
int a[5001],dp[5001],n,ans;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
    {
        dp[i]=1;
        for(int j=1;j<i;j++)
            if(a[j]>a[i])
                dp[i]=max(dp[i],_____+1);
        ans=max(ans,dp[i]);
    }
    printf("%d",ans);
    return 0;
}

T3 簡單傳紙條

難度:入門+, 做法:你猜

很顯然這題的做法是 dfs DP 。設 \(dp_{i,j}\) 為走到座標 \((i,j)\) 的最小八卦值之和。那麼由於點 \((i,j)\) 只能由點 \((i-1,j)\)\((i,j-1)\) 到達,所以狀態轉移方程為 \(dp_{i,j}=min(dp_{i-1,j},dp_{i,j-1}+a_{i,j})\)

Code:

#include <bits/stdc++.h>
using namespace std;
int n,m,a[2200][2200],dp[2200][2200];
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&a[i][j]);
    memset(dp,0x3f,sizeof(dp));
    dp[1][1]=a[1][1];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            dp[i][j]=min(dp[i][j],min(dp[i-1][j],dp[i][j-1])+_______);
    printf("%d",ans);
    return 0;
}

T4 數字三角形

難度:NOI/NOI+/CTSC, 做法:膜你退火 記憶化搜尋

這是IOI 1994原題。

做法為從頂端的點開始向下dfs,回朔時記錄下當前點的答案,以此避免 \(O(2^n)\) 的暴力搜尋,時間複雜度變為 \(O(n^2)\)

Code:

#include <bits/stdc++.h>
using namespace std;
int n,a[2200][2200],dp[2200][2200];
int dfs(int dep,int x)
{
    if(dep>n || x>dep)  return 0;
    if(dp[dep][x])    return dp[dep][x];
    dp[dep][x]=max(dp[dep][x],dfs(dep+1,x));
    dp[dep][x]=max(dp[dep][x],dfs(dep+1,x+1));
    return dp[dep][x]+=_________;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            scanf("%d",&a[i][j]);
    printf("%d",dfs(1,1));
    return 0;
}

T5 遞迴函式

難度:普及-, 做法:(偽)暴力

直接將遞迴公式原原本本抄下來,加上記憶化即可。

注:需要一定的玄學優化,不然會不明不白地TLE

Code:

#include<bits/stdc++.h>
using namespace std;
int a,b,c,ans,dp[60][60][60];
int w(int x,int y,int z)
{
    if(x<=0 || y<=0 || z<=0)
        return dp[0][0][0]=1;
    if(dp[x][y][z])
        return dp[x][y][z];
    if(x>20 || y>20 || z>20)
        return dp[x][y][z]=w(20,20,20);
    if(x<y && y<z)
        return dp[x][y][z]=w(x,y,z-1)+w(x,y-1,z-1)-w(x,y-1,z);
    return dp[x][y][z]=w(x-1,y,z)+w(x-1,y-1,z)+w(x-1,y,z-1)-w(x-1,y-1,z-1);
}
int main()
{
    while(true)
    {
        scanf("%d%d%d",&a,&b,&c);
        if(a==-1 && b==-1 && c==-1)
            break;
        if(a<=0 || b<=0 || c<=0)
            ans=1;
        else if(dp[a][b][c]==0)
            ans=w(a,b,c);
        else
            ans=dp[a][b][c];
        printf("w(%d, %d, %d) = %d\n",______,ans);
    }
    return 0;
}

T6 雷曼兔

題目傳送 戳這

前置知識:線性dp

1.審題

簡明題意

有一個 N*N 的矩陣,矩陣元素由1~2*N的數字組成。當從點(x1,y1)向點(x2,y2)跳時將獲得收益 v=(|x1-x2|+|y1-y2|)^2 。找到一條路徑使收益 最大


2.思路

貪心看似有顯然的策略(即每一次尋找受益最大的點,然後跳過去)
很遺憾,手玩了幾組樣例後就心灰意冷

就只好向dp的方向思考
首先這是一道 基礎的 線性dp(相信聰明的你看得出來)

  • 定義: dp[i] 表示從i層到最底層的最大華麗度總和
  • 初始化:從i層直接到1層的華麗度
  • 狀態轉移方程: dp[i]=max{dp[j]+v} (very important)
    由於v=(|x1-x2|+|y1-y2|)^2,所以v始終大於0,dp[n*n]即為答案!
    題做完了!!

3.實現

#include<bits/stdc++.h>
using namespace std;
const int N=3010;
int n;
int s[N][3],dp[N];
int Abs(int i,int j){
	return pow((abs(s[i][1]-s[j][1])+abs(s[i][2]-s[j][2])),2);
}
int main(){
	scanf("%d",&n);
	int num;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			cin>>num;
			s[num][0]=num,s[num][1]=i,s[num][2]=j;
		}
	}
	for(int i=n*n;i>0;i--){
		dp[i]=Abs(i,1);
	}
	for(int i=2;i<=n*n;i++){
		int ma=dp[i];
		for(int j=1;j<i;j++){
			if(ma<dp[j]+Abs(i,j)){
				ma=dp[j]+Abs(i,j);
			}
		}
		dp[i]=ma;
	}
	printf("%d\n",dp[n*n]);
	return 0;
}

T7 核電站問題

題目傳送 戳這

前置知識:線性dp

1.審題

簡明題意

實際上題目已經挺簡單了,就不給簡明題意了。


2.思路

這還是一道線性dp(好吧,直接深搜也可以AC

  • 定義: dp[i] 表示放到第i個坑時的總方案數。
  • 考慮任意i:

1.當(i<m)的時候,也就是說總數比核物質要少,這樣子怎麼樣組合也不會爆炸
2.當(i==m)的時候,只會有一種情況——這i個位置全是核物質,所以要減去1
3.當(i>m)的時候,這時候就要考慮爆炸的情況了下面來討論這個遞推式

我們的思路是拿每一次情況減去這次要減去行不通(爆炸)的情況
因為每一次都考慮前面不行的情況所以接下來迴圈只需要考慮這次不行的情況
總情況為2*a[i-1]。

我們給第i為設定一個放和不放的狀態,也就是0和1(1為核物質,0為木有)
前面i-1已經考慮好了,都可以放,所以先不用處理加上這第i位的情況,根據組合原理,第i位兩種情況所以一共2*a[i-1]種情況
接下來就是處理不行(爆炸)的情況

當迴圈到第i位的時候,會爆炸的情況就是前面已經有m-1個1(核物質),但這裡m-1個1的前面那一位一定不是1!
若為1的話早就在前面達到m的情況下已經處理(-)過了
所以前面從開始數一共i-1-m位是可以任意組合 的,01001001…任意,因為前面是不會有核物質爆炸的
根據組合原理到達m個爆炸的一共有前面i-1-m個數的組合,所以減去a[i-1-m]


3.實現

短的可憐

#include<stdio.h>
using namespace std;
int main(){
    int n,m,i;
    while(scanf("%d%d",&n,&m)!=EOF){
    	long long dp[n+1];
        dp[0]=1;
        for(i=1;i<=n;i++){
            if(i<m) dp[i]=dp[i-1]*2;
            else if(i==m) dp[i]=dp[i-1]*2-1;
            else dp[i]=dp[i-1]*2-dp[i-m-1];
        }
        printf("%lld ",dp[n]);
    }
} 

T8 統計單詞個數

題目傳送 戳這

前置知識:雜湊,二維dp

1.審題

簡明題意

給出一個字串,將其分成k段,使每份中包含的單詞個數加起來總數最大

2.思路

不會處理字串,如何判重?

雜湊!!

先定義狀態:

  • dp[i][j]表示分到第i個位置,分了j次的最優解
  • map[i][j]表示在從i到j的區間裡有合法的子串的個數

預處理:分別把每一個子串雜湊一遍和目標串匹配,匹配完成將這一段加1;
狀態轉移方程: dp[i][j]=max(dp[i][j],dp[1~i-1][j-1])
特別地:當j=1時,dp[i][1]=map[0][i]


3.實現

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int ran=233;
int n,k,s;
char ch[300];
char f[9][100];
char t[300];
int ok[9],ll[9];
int mp[300][300];
int dp[300][300];
int haxi(char c[]){
    int le=strlen(c);
    int an=0;
    for(int i=0;i<le;i++)an=(an*ran+c[i])%mod;
    return an;
}
bool ha(int x,int y){
    for(int i=1;i<=s;i++){
    memset(t,0,sizeof(t));
    strncpy(t,ch+x,min(ll[i],y-x+1)); 
    if(haxi( t )==ok[i])return 1;
    }
    return 0;
}
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
    	cin>>ch+(i*20-20);
	}
    cin>>s;
    for(int i=1;i<=s;i++){
    	cin>>f[i];
	}
    for(int i=1;i<=s;i++){
    	ll[i]=strlen(f[i]);
	}
    for(int i=1;i<=s;i++){
    	ok[i]=haxi(f[i]);
	}
    int len=strlen(ch);
    for(int i=len-1;i>=0;i--){
        for(int j=i;j>=0;j--){
            mp[j][i]+=mp[j+1][i];
            if(ha(j,i)){
            	mp[j][i]++;
			}
        }
    }
    for(int i=0;i<len;i++){
    	for(int j=1;j<=k;j++){
    		if(j==1){
    			dp[i][j]=mp[0][i];
			}else{
				for(int l=0;l<i;l++){
					if(dp[l][j-1]){
						dp[i][j]=max(dp[i][j],dp[l][j-1]+mp[l+1][i]);
					}
				}
			}
		}
	}
    cout<<dp[len-1][k];
    return 0;
}

T9 環形石子歸併

題目傳送 戳這

前置知識:區間dp,字首和

1.審題

請自行閱讀題目!(相信你讀過,NOI1995真題喔


2.思路

最小得分? 難道是貪心
貪心顯然不能,因為只有相鄰兩堆才能合併!

Hack:

5
8 4 6 3 5

所以,此題正解是區間dp

定義(i<j ):

  • dp1[i][j]表示把從i到j的石子合併為一堆的最小得分
    狀態轉移方程: dp1[i][j]=min(dp1[i][j],dp1[i][k]+dp1[k+1][j]+a[i]+a[i+1]+……+a[j])
    我們發現要求一段區間和,所以毫不猶豫上字首和
    dp要先列舉dp區間的長度,再列舉左端點,右端點也可以求出來,再列舉k。

3.實現

#include<bits/stdc++.h>
using namespace std;   
int n,minl,dp1[300][300],num[300];  
int s[300];  
int d(int i,int j){
	return s[j]-s[i-1];
}
int main(){   
    scanf("%d",&n);  
    for(int i=1;i<=n;i++) {
        scanf("%d",&num[i]);  
        num[i+n]=num[i];  
    }  
    for(int i=1;i<=2*n;i++){
    	s[i]=s[i-1]+num[i]; 
    }
    for(int p=1;p<n;p++){
        for(int i=1,j=i+p;(j<n+n)&&(i<n+n);i++,j=i+p){  
            dp1[i][j]=999999999;  
            for(int k=i;k<j;k++){     
                dp1[i][j]=min(dp1[i][j],dp1[i][k]+dp1[k+1][j]+d(i,j));  
            }  
        }  
    }  
    minl=999999999;  
    for(int i=1;i<=n;i++){ 
        minl=min(minl,dp1[i][i+n-1]);  
    }  
    printf("%d\n",minl);  
    return 0;  
}

T10 乘法遊戲

題目傳送 戳這

前置知識:區間dp

1.審題

簡明題意

給出一串數,取出一個數(不能是頭和尾),將獲得的收益是它左邊,右邊和它自己的乘積。求最小收益。


2.思路

考慮一個區間的最優解,設這一段長度為L,最終要求的是長度為n的那段的最優解。
令這個區間為i~j,則i~j中取一點k(i<k<j)。
那i~j區間的最優解就是i~k區間的最優解加上k+1~j區間的最優解再加上合併這兩個區間所需的代價。
但是要i~j區間最優,所以要列舉k從i+1~j-1的所有情況,取最優的那一組。
定義:dp[i][j]表示i~j區間的最優解
狀態轉移方程: dp[i][i + k - 1] = dp[i][j] + dp[j][i + k - 1] + (a[i] * a[i + k - 1] * a[j]);


3.實現

#include<bits/stdc++.h>
using namespace std;
const int N=110;
int n;
int a[N],dp[N][N];
int main(){
	scanf("%d",&n);
	memset(dp,0x3f3f3f3f,sizeof(dp));
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]); 
	}
	for(int i=1;i<=n;i++){
		dp[i][i]=a[i];
		dp[i][i+1]=0;
	}
	for(int p=2;p<=n-1;p++){
		for(int i=1;i<=n-1;i++){
			int j=i+p;
			if(j>n){
				continue;
			}
			for(int k=i+1;k<=j-1;k++){
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]);
			}
		}
	}
	printf("%d",dp[1][n]);
	return 0;
}

動態規劃基礎2

Problem A:01揹包

洛谷原模板題戳這兒

這是一道\(Dp\)的模板題,也沒什麼好說的,直接開始吧

一、審題

有N件物品和一個容量為\(V\)的揹包。第\(i\)件物品所佔空間是\(C_i\),價值是\(W_i\)。求解將哪些物品裝入揹包可使價值總和最大。

資料範圍:\(0 \le V \le 1000,0\le N\le 100,0 < C \le 1000,0 < W \le 100\)

似乎並沒有什麼關鍵點,粗略判斷時間複雜度,\(emm······\)\(O(NV)\)是可以過的,那就往這方面想吧

二、做題

0、貪心

若果你還沒學過\(Dp\),就很容易會想到用貪心來做這道題,設一個數組\(sum_i=\dfrac{W_i}{C_i}\),然後優先但是就會出現一個很大的問題:假設我們揹包的容量\(C=10\),有以下三個物品

物品編號 A B C
C 6 5 5
W 9 6 6
Sum 1.5 1.2 1.2

根據以上的貪心策略,我們應該是要選\(A\)的,但是我們發現同時選\(B\)\(C\)的話得到的價值會更大呀,貪心思想不能從大局考慮,所以必須要用\(Dp\)

1、揹包

既然確定了是動規,那就可以愉快地開始推式子啦

讓我假設現在的揹包的容量C=10;

物品編號 物品重量 物品價值
1 5 20
2 6 10
3 4 12

我們用\(v[i]\)表示物品價值,\(w[i]\)表示物品重量,再首先定義狀態$ dp[i][j]\(以\)j$ 為容量為放入前\(i\)個物品(按\(i\)從小到大的順序)的最大價值,那麼 \(i=1\)的時候,放入的是物品\(1\),這時候肯定是最優的啦!

那考慮一下當前容量\(j\),如果 \(j<5\),那麼肯定就不能放,所以\(dp[1][j]=0(j<5)\);那如果 \(j>5\),那就可以放進去了的呀,所以\(dp[1][j]=20(j>=5)\)

接著 i=2i=2 放兩個物品,求的就是 \(dp[2][j]\) 了,當 $j<5 $的時候,是不是同樣的 \(dp[2][j](j<5)=0\);那當 \(j<6\) 是不是還是放不下第二個,只能放第一個;

\(j>6\) 呢?是不是就可以放第二個了呢?是可以,但是明顯不是最優的,用腦子想了一下,發現 \(dp[2][j](j>6)=20\),這個 \(20\)怎麼來的呢,當然是從前一個狀態來的(注意這裡就可以分為兩種情況了):一種是選擇第二個物品放入,另一種還是選擇前面的物品;

我們假設\(j=10\),這時候:\(dp[2][10]=max((dp[1][10-w[2]])+v[2],dp[1][10])\)也就是\(dp[2][10] = max(dp[1][4])+10,dp[1][10])\)

是不是很明顯了呢,\(dp[1][4]+10\)是選擇了第二個,於是容量相應就減少成 \(4\),之前已經得出 \(dp[1][4]=0\),就是說選了物品\(2\),物品\(1\)就選不了了;\(dp[1][10]\)是不選擇第二個,只選擇第一個\(dp[1][10]\)是等於\(20\)的,於是得出\(dp[2][10]=20\)

到這裡就可以了,依次類推,動態轉移方程為

\[dp[i][j] = max(dp[i-1][j-w[i]])+v[i],dp[i-1][j] \]

但是好像還有一些問題沒考慮完.........

2、需要單獨考慮的問題

看回例子:

物品編號 物品重量 物品價值
1 5 20
2 6 10
3 4 12

我們知道\(dp[1][j](j<5)=20\),那麼\(dp[2][j](j=5)\)的時候是多少呢?我們看到動態轉移方程並沒有考慮 \(j<w[i]\) 的情況,但是我們可以加進去,由於我們可以看出來\(dp[2][5]=5\),為什麼?因為不能選第二個,只能選第一個,所以\(dp[2][5]=dp[1][5]\)了!所以當\(j<w[i]\)的時候,\(dp[i][j]=dp[i-1][j]\)就好了,是不是很神奇呢?

3、一維優化

我們剛是用二維來存狀態的,那可不可以壓縮到一維呢?

答案是可以的,其實我們發現上面的\(i\)是可以省去的,但這個時候就會有人說了:物品會重複放入。所以重點就是,一維內層迴圈要倒著來!不然會重複放入

三、程式碼

1、二維陣列

#include<bits/stdc++.h>
using namespace std;
int w[105],val[105];
int dp[105][1005];
int main()
{
	int t,m,res=-1;
	scanf("%d%d",&t,&m);
	for(int i=1;i<=m;i++) scanf("%d%d",&w[i],&val[i]);
	for(int i=1;i<=m;i++)
	for(int j=t;j>=0;j--)
	if(j>=w[i]) dp[i][j]=max(dp[i-1][j-w[i]]+val[i],dp[i-1][j]);
	else dp[i][j]=dp[i-1][j];
	printf("%d",dp[m][t]);
	return 0;
}

2、一維陣列

#include<bits/stdc++.h>
using namespace std;
int aa[1000001],bb[1000001],f[1000001];
int a,b,c,d;
int main()
{
	scanf("%d%d",&a,&b);
	for(int i=1;i<=b;++i) scanf("%d%d",aa+i,bb+i);
	for(int i=1;i<=b;i++)
	{
		for(int j=aa[i];j<=a;j++)
		{
			f[j]=max(f[j],f[j-aa[i]]+bb[i]);
		}
	}
	printf("%d",f[a]);
}

Problem B:完全揹包

GDFZOJ原題地址戳這兒

洛谷原模板題戳這兒

這是一道\(Dp\)的模板題,也沒什麼好說的,直接開始吧

一、審題

有N件物品和一個容量為\(V\)的揹包。每種物品均有無窮多件,第\(i\)件物品所佔空間是\(C_i\),價值是\(W_i\)。求解將哪些物品裝入揹包可使價值總和最大。

資料範圍:\(0 \le V \le 1000,0\le N\le 100,0 < C \le 1000,0 < W \le 100\)

似乎並沒有什麼關鍵點,粗略判斷時間複雜度,\(emm······\)\(O(NV)\)是可以過的,那就往這方面想吧

二、做題

我們發現這道題是不是和這道題這道題很像?是的這兩道題之間的差異只在這一句話\(\text{“每種物品均有無窮多件”}\),所以可以推斷出這道題的式子一定和上一道題很像,所以建議先看一看上一題的題解

在上一篇題解裡我們在一維陣列中說到\(\text{重點就是,一維內層迴圈要倒著來!不然會重複}\),這是在01揹包中需要做的,但是在這裡就不需要了呀,反正每種物品都有無窮多件,為什麼要考慮重合呢?

所以直接從小到大列舉就行啦!!!

三、程式碼

#include<bits/stdc++.h>
using namespace std;
int aa[1000001],bb[1000001],f[1000001];
int a,b,c,d;
int main()
{
	scanf("%d%d",&a,&b);
	for(int i=1;i<=b;++i) scanf("%d%d",aa+i,bb+i);
	for(int i=1;i<=b;i++)
	{
		for(int j=aa[i];j<=a;j++)
		{
			f[j]=max(f[j],f[j-aa[i]]+bb[i]);
		}
	}
	printf("%d",f[a]);
}

Problem C:多重揹包

這是一道\(Dp\)的模板題,也沒什麼好說的,直接開始吧

一、審題

有N件物品和一個容量為\(V\)的揹包。每種物品均有有限多件,第\(i\)件物品所佔空間是\(C_i\),價值是\(W_i\)。求解將哪些物品裝入揹包可使價值總和最大。

資料範圍:\(0 \le V \le 1000,0\le N\le 100,0 < C \le 1000,0 < W \le 100\)

似乎並沒有什麼關鍵點,粗略判斷時間複雜度,\(emm······\)\(O(NV)\)是可以過的,那就往這方面想吧

二、做題

我們發現這道題是不是和這道題這道題很像?是的這兩道題之間的差異只在這一句話\(\text{“每種物品均有有限多件”}\),所以可以推斷出這道題的式子一定和上一道題很像,所以建議先看一看上一題的題解

其實這也不是很簡單嗎?既然他有有限個物品,那把它轉化為多個一樣的物品不就做出來了嗎?所以只需要在01揹包的基礎上再加上預處理就行啦

還不會01揹包的人戳這兒

三、程式碼

#include<bits/stdc++.h>
using namespace std;
int weight[10001],value[10001],num[10001],f[10001];
int a,b;
int main()
{
	scanf("%d%d",&a,&b);
    for(int i=1;i<=a;++i) scanf("%d%d%d",weight+i,value+i,num+i);
	int k=a+1;
	for(int i=1;i<=a;++i)
	while(num[i]!=1)
	{
		weight[k]=weight[i];
		value[k]=value[i];
		k++;
		num[i]--;
	}
    for(int i=1;i<=k;i++)
	for(int j=b;j>=1;j--)
	if(weight[i]<=j) f[j]=max(f[j],f[j-weight[i]]+value[i]);
    cout<<f[b]<<endl;
    return 0;
}

Problem D:過河卒

GDFZOJ原題地址戳這兒

洛谷原題地址戳這兒

一、審題

\(A\)點有一個過河卒,需要走到目標B點。卒行走規則:可以向下、或者向右。同時在棋盤上的任一點有一個對方的馬(如上圖的\(C\)點),該馬所在的點和所有跳躍一步可達的點稱為對方馬的控制點。例如上圖\(C\)點上的馬可以控制\(9\)個點(圖中的\(P1,P2…P8\)\(C\))。卒不能通過對方馬的控制點。

棋盤用座標表示,\(A\)\((0,0)\)\(B\)\((n,m)\)\(n,m\)為不超過\(20\)的整數),同樣馬的位置座標是需要給出的。現在要求你計算出卒從\(A\)點能夠到達\(B\)點的路徑的條數。

很明顯,這道題用暴搜是不行的,所以要用\(Dp\)

二、做題

我們首先定義狀態,我們設\(f_{i,j}\)為從點\((0,0)\)走到點\((i,j)\)一共有多少種方案,然後就可以開始愉快地推式子啦

首先我們知道\(f_{i,j}\)可以從\(f_{i-1,j}\)\(f_{i,j-1}\)轉移過來,因為如果要走到點\((i,j)\),就一定要麼要經過\((i-1,j)\),要麼要經過\((i,j-1)\),而且只能從這兩個點走過來。所以我們可以很容易就推出一個式子:

\[f_{i,j}=f_{i-1,j}+f_{i,j-1} \]

可是我們還有“馬”沒有處理呢!其實這個也很簡單,因為沒法走到被馬控制的點,所以只需要針對馬控制的點\((i,j)\),將\(f_{i,j}\)改為0就行了

所以我們就順利地推出來了

\[f_{i,j}=\begin{cases}f_{i,j-1}+f_{i-1,j}&(\text{點}(i,j)\text{不為被馬控制的點})\\0&(\text{點}(i,j)\text{為被馬控制的點})\end{cases} \]

三、程式碼

#include <bits/stdc++.h>
using namespace std;
long long int a,b,n,m,aa[22][22],zou[23][23];
void bj(long long int x,long long int y)
{
    zou[x][y]=1;
    zou[x-1][y-2]=1;
    zou[x-2][y-1]=1;
    zou[x-2][y+1]=1;
    zou[x-1][y+2]=1;
    zou[x+1][y-2]=1;
    zou[x+2][y-1]=1;
    zou[x+2][y+1]=1;
    zou[x+1][y+2]=1;
}
int main()
{
    cin>>n>>m>>a>>b;
    bj(a,b);
    aa[1][0]=1;
    for(int i=1;i<=n+1;++i)
    {
        for(int j=1;j<=m+1;++j)
        {
            aa[i][j]=aa[i-1][j]+aa[i][j-1];
            if(zou[i-1][j-1]) aa[i][j]=0;
        }
    }
    printf("%lld",aa[n+1][m+1]);
    return 0;
}

Problem E:騎士遊歷

一、審題

給定一個信封,最多隻允許貼上\(n(n\le100)\)張郵票,我們現在有\(m(m\le100)\)種郵票,面值分別為:\(x_1,x_2,······,x_m(xi\le255,\text{為正整數})\)分,並假設各種郵票都有足夠多張。要求計算所能獲得的郵資最大範圍。即求最大值\(MAX\),使在\(1-MAX\)之間的每一個郵資值都能得到。

很明顯,這是一道\(Dp\)的題目

二、做題

三、程式碼

#include<bits/stdc++.h>
using namespace std;
int a,b,i,j,ans;
int f[2000000];
int main() 
{
	scanf("%d%d",&a,&b);
	for(i=1;i<=2000000;i++) f[i]=1e9;
	f[0]=0;
	for (i=1;i<=b;i++)
	{
		int x;
		scanf("%d",&x);
		for(j=x;j<=2000000;j++)
		if(f[j-x]+1<=a) f[j]=min(f[j],f[j-x]+1);
    } 
	ans=0;
	for(i=1;i<=2000000;i++)
	if(f[i]==1e9)
	{
		ans=i-1;
		break;
	}
	printf("MAX=%d\n",ans);
    return 0;
}

Problem F:騎士遊歷

Answer

我們考慮一個點能在哪個點跳過來,設當前點的座標是(x,y),則能在點(x - 2,y - 1),(x - 2,y + 1),(x - 1,y - 2),(x - 1,y + 2)跳過去。

那麼就很容易想到,設f[x][y]表示從起點到點(x,y)的方案數,狀態轉移方程為:f[x][y] = f[x - 2][y - 1] + f[x - 2][y + 1] + f[x - 1][y - 2], f[x - 1][y + 2]。

需要注意的是,要判斷陣列越界(國際慣例),本題座標從左下角開始,答案要開long long。

Code

#include<bits/stdc++.h>
#define x1 pos[0].first
#define y1 pos[0].second
#define x2 pos[1].first
#define y2 pos[1].second
using namespace std;
const int N = 55;
int a[N][N],n,m;
long long f[N][N];
pair<int,int> pos[2];
int main(){
	scanf("%d%d%d%d%d%d",&n,&m,&x1,&y1,&x2,&y2);
	f[x1][y1] = 1;
	for(int i = x1 + 1;i <= x2;i++){
		for(int j = 1;j <= n;j++){
			if(i - 2 >= 1 && j - 1 >= 1){
				f[i][j] += f[i - 2][j - 1];
			}				
			if(i - 2 >= 1 && j + 1 <= n){
			    f[i][j] += f[i - 2][j + 1];
			}               
			if(i - 1 >= 1 && j - 2 >= 1){
			    f[i][j] += f[i - 1][j - 2];
			}
			if(i - 1 >= 1 && j + 2 <= n){
			    f[i][j] += f[i - 1][j + 2];
			}								
		}
	}
	printf("%lld",f[x2][y2]);
}

Problem G:乘積最大

Answer

這題資料很水,可以直接dfs

首先,因為輸入的是一個字串,我們先將字串轉化成一個數組。設a[i][j]表示擷取第i個數字到第j個數字所表示的數值。

然後,因為這題是一道區間dp,所以我們設f[i][j]表示前i位插入j個乘號所得到的最大值。

很容易想到初始數值是前i位如果不插入乘號,那麼答案就是前i位表示的數值,即f[i][0] = a[0][i]。

最後,狀態轉移方程為f[i][j] = max(f[pos][j-1] * a[pos + 1][i],f[i][j]),其中f[pos][j - 1] * a[pos + 1][i]表示在第pos個數字和第pos + 1個數字之間插入一個乘號時的最大值。(pos從0到i - 1)

Code

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
using namespace std;
const int N = 15;
int n,k,f[N][N],a[N][N];
char s[N];
int main(){
	scanf("%d%d",&n,&k);
	scanf("%s",s);
	rep(i,0,n - 1){
		int x = 0;
		for(int j = i;j < n;j++){
			x = x * 10 + s[j] - '0';
			a[i][j] = x;
		}
	}
	rep(i,0,n - 1){
		f[i][0] = a[0][i];
	}
	rep(i,0,n - 1){
		rep(j,1,k){
			rep(pos,0,i - 1){
				f[i][j] = max(f[pos][j - 1] * a[pos + 1][i],f[i][j]);
			}
		}
	}
	printf("%d",f[n - 1][k]);
}

Problem H:沒有上司的舞會

Answer

其實這題是一道樹形dp,我也不知道為什麼在動態規劃基礎2裡面

首先顯而易見,可以設f[u]表示以i為根的子樹上最大的快樂指數,但由於每個人是否參加舞會會對他的下屬能否參加舞會有影響,也就是說有後效性,所以僅僅這樣設計是不夠的。

考慮加第二維陣列,f[u][0/1]表示以u為根的子樹的最大的歡樂指數,0表示u不去,1表示u去。

那麼狀態轉移方程就比較容易了:

當u不去時,它的兒子節點v可去可不去,所以我們取最大值,即f[u][0] += max(f[v][0],f[v][1])。

當u去時,它的兒子節點v只能不去,即f[u][1] += f[v][0]。

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 6010;
struct edge{
	int v,next;
}e[N];
int head[N],n,cnt,f[N][2],ans,vis[N];
void add_edge(int u, int v){
	e[++cnt].v = v;
	e[cnt].next = head[u];
	head[u] = cnt;
}
void dfs(int u){
    vis[u] = 1;
    for(int i = head[u];i;i = e[i].next){
		int v = e[i].v;
		if(vis[v]){
			continue;
		}
		dfs(v);
		f[u][1] += f[v][0];
		f[u][0] += max(f[v][0],f[v][1]);
	}
}
int main() {
	scanf("%d",&n);
	for(int i = 1;i <= n;i++){
		scanf("%d",&f[i][1]);
	} 
	for(int i = 1;i < n;i++){
    	int u,v;
		scanf("%d%d",&u,&v);
	    vis[u] = 1;
	    add_edge(v,u);
	}
	for(int i = 1;i <= n;i++){
		if(!vis[i]){
	      	dfs(i);
	      	printf("%d",max(f[i][1],f[i][0]));
	      	return 0;
		}	
	}  
}

Problem I:尋寶

Answer

設f[u][i]表示第i個節點用j天取到的最多寶藏。

初始數值是第u個節點第0天取到的最多寶藏就是v[u],即f[u][0] = v[u]。

狀態轉移方程為f[u][j] = max(f[u][j],f[u][j - k - w * 2] + f[v][k]),其中,f[u][j - k - w * 2]的意思是當前子節點v用k的時間,w * 2是從u到v再回到u的時間。

Code

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define per(i,a,b) for(int i = a;i >= b;i--)
const int N = 105;
const int M = 205;
using namespace std;
struct edge{
	int v,w,next;
}e[N];
int k,n,m,ans,cnt,v[N],f[N][M],head[N];
inline void add_edge(int u,int v,int w){
	e[++cnt].v = v;
	e[cnt].w = w;
	e[cnt].next = head[u];
	head[u] = cnt;
}
inline void dfs(int u,int fa){
	f[u][0] = v[u];
	for(int i = head[u];i;i = e[i].next){
		int v = e[i].v;
		int w = e[i].w;
		if(v != fa){
			dfs(v,u);
			per(j,m,1){
				per(k,j - w * 2,0){
					f[u][j] = max(f[u][j],f[u][j - k - w * 2] + f[v][k]);
				}
			}
		}
	}
}
inline void init(){
	cnt = ans = 0;
	memset(f,0,sizeof(f));
	memset(head,0,sizeof(head));
	memset(v,0,sizeof(v));
}
int main(){
	while(scanf("%d",&n) != EOF){
		rep(i,1,n){
			scanf("%d",&v[i]);
		}
		rep(i,1,n - 1){
			int u,v,w;
			scanf("%d%d%d",&u,&v,&w);
			add_edge(u,v,w);
			add_edge(v,u,w);
		}
		scanf("%d%d",&k,&m);
		dfs(k,0);
		rep(i,0,m){
			ans = max(f[k][i],ans);
		}
		printf("%d\n",ans);
		init();
	}
}

Problem J:完美伺服器

Answer

設f[u][0/1/2]表示以u為根的子樹的最少要設定伺服器的數量。

其中,

f[u][0]表示u是伺服器,此時每個子結點可以是也可以不是。狀態轉移方程為f[u][0] += min(f[v][0],f[v][1])。

f[u][1]表示u不是伺服器,但u的父親是,此時u的子結點都不是伺服器。狀態轉移方程為f[u][1] += f[v][2]。

f[u][2]表示u和u的父親都不是伺服器,此時u的子結點恰有一個是伺服器。狀態轉移方程為f[u][2] = min(f[u][2],f[u][1] + f[v][0] - f[v][2])。

初始值為f[u][0] = 1,f[u][2] = INF。

需要注意的是,INF不能太大,否則f[u][2] 會爆掉。

Code

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
using namespace std;
const int N = 10010;
struct edge{
	int v,next;
}e[N * 2];
int n,cnt,f[N][3],head[N];
inline void add_edge(int u,int v){
	e[++cnt].v = v;
	e[cnt].next = head[u];
	head[u] = cnt;
}
inline void dfs(int u,int fa){
	f[u][0] = 1;
	f[u][2] = N;
	for(int i = head[u];i;i = e[i].next){
		int v = e[i].v;
		if(v != fa){
			dfs(v,u);
			f[u][0] += min(f[v][0],f[v][1]);
        	f[u][1] += f[v][2];
		}
	}
	for(int i = head[u];i;i = e[i].next){
		int v = e[i].v;
		if(v != fa){
			f[u][2] = min(f[u][2],f[u][1] + f[v][0] - f[v][2]);
		}
    }
}
inline void init(){
	memset(f,0,sizeof(f));
	memset(head,0,sizeof(head));
	memset(e,0,sizeof(e));
	cnt = 0;
}
int main(){
	while(scanf("%d",&n)){
		if(n == 0){
			continue;
		}
		if(n == -1){
			return 0;
		}
		rep(i,1,n - 1){
			int u,v;
			scanf("%d%d",&u,&v);
			add_edge(u,v);
			add_edge(v,u);
		}
		dfs(1,0);
		printf("%d\n",min(f[1][0],f[1][2]));
		init();
	}
}

寫在後面

版權人

本題解是由四位大佬的題解拼合而成,分別是:The_NobodyzhnzhRoyRoyExplodingKonjac(排名不分先後)

來源

(來源不分先後)

Galaxy OJ動態規劃基礎1——T1~T5

動態規劃基礎2-Problem 6~10

題解 GDFZOJ 【649】 郵票問題

題解 GDFZOJ 【661】 過河卒

題解 GDFZOJ 【648】 多重揹包

題解 GDFZOJ 【647】 完全揹包

題解 GDFZOJ 【646】 01揹包

GFoj 動態規劃基礎1(6-10)題解

未經允許,不得轉載!!!