DP 從棺材到入土

區間DP

P1063 能量項鍊

題目描述
  • 給定一串首尾相連的能量珠串
  • 按照該計算規則進行合併:若是前一顆能量珠的頭標記爲\(m\),尾標記爲\(r\),後一顆能量珠的頭標記爲\(r\),尾標記爲\(n\),則聚合後釋放的能量爲\(m \times r \times n\),新產生的珠子的頭標記爲\(m\),尾標記爲\(n\)
  • 求最終合併爲一個珠子的時候釋放的能量的最大值
思路分析
  • 首先由於只是一個串串,因此咱們確定很差弄,因此咱們能夠生成一個線性的區間來代替這個串串
  • 那麼根據這個規則,咱們分爲好幾個小區間進行\(dp\),相似於弗洛伊德的算法,枚舉不一樣長度的區間進行比較大小
  • 咱們很顯然的能夠知道從\(i~n+i-1\)是一個完整的能量珠串(減去\(1\)是由於他本身已經有了),因此咱們能夠根據這個作dp了
  • 枚舉長度\(k\),因此咱們能夠看出在\(i~i+k\)這個區間內,咱們能夠找一個點\(j\)來分割開,至關於已經合併完的兩個珠子\(i~j\)\(j+1~i+k\),最終在進行合併
狀態轉移方程設計
  • \(f_{i,j}爲合併第\)i~j$個石子的最優值
  • 因此咱們根據思路能夠推導出式子

\[f[i,i+k]=max(f[i,i+k],f[i,j]+f[j+1,i+k]+left_i\times right_j\times right_{i+k})\ \ (j\in [i,i+k)) \]

代碼實現
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=410;
int f[N][N];
int n;
struct node{
	int x,y;
}a[N];
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i].x;
		a[i-1].y=a[i].x;
	}
	a[n].y=a[1].x;
	for(int i=1;i<n;i++)
	a[n+i]=a[i];//使其成爲一條鏈,方便操做
	//i~i+n就能夠當作一個完整的鏈 
	memset(f,0,sizeof(f));
	//f[i][j]表示合併第i~j個石子的最優值 
	for(int k=1;k<=n;k++)//枚舉長度
		for(int i=1;i<2*n-k;i++)//從第i個石子開始斷開,
		//那麼i~i+n就是一條線 
		 	for(int j=i;j<i+k;j++)
		 	//在i~i+k中任意選一點j做爲分界點,而後就能夠分紅
			 //i~j和j+1~i+k這兩段 
			 //首先這兩段裏面的都合併起來,而後最後(i,j)(j+1,i+k)端點序號爲這兩個的
			 //珠子在盡心合併  
		 	{
		 		f[i][i+k]=max(f[i][i+k],f[i][j]+f[j+1][i+k]+a[i].x*a[j].y*a[i+k].y); 
			} 
	int ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i][i+n-1]);
	cout<<ans<<endl;
	return 0;					   
}

P1880 [NOI1995]石子合併

題目描述

在一個圓形操場的四周擺放 \(N\) 堆石子,現要將石子有次序地合併成一堆.規定每次只能選相鄰的\(2\)堆合併成新的一堆,並將新的一堆的石子數,記爲該次合併的得分。node

試設計出一個算法,計算出將 \(N\) 堆石子合併成 \(1\) 堆的最小得分和最大得分。ios

思路分析
  • 和能量項鍊這個題目相相似,仍然是一個環形的區間dp,咱們能夠從其中的任意一堆石子開始操做
  • 咱們能夠把這個多開\(n\)的數組空間,將他弄成線性進行解決
狀態轉移方程設計
  • 區間dp,因此咱們最多見的方法是狀態轉移方程設置爲區間的形式算法

  • 最多見的作法爲在一個大的區間內找兩個區間並進行合併,求最大值數組

  • 這個題咱們經過數據能夠發現,當區間\([i,j]\)中兩個小的區間\([i,k],[k+1,j]\)合併時,它此次合併的得分正好爲第\(i\)堆到第\(j\)堆的石子的總個數
    因此咱們設\(s_i\)爲前\(i\)堆石子的前綴和spa

  • 咱們能夠設置\(f[i][j]表示從第\)i\(堆到第\)j$堆合併成爲\(1\)堆時的區間最值,能夠獲得如下的狀態轉移方程式翻譯

\[f_{max}[i][j]=max(f_{max}[i][j],f_{max}[i][k]+f_{max}[k+1][j]+s_j-s_{i-1})\ \ \ \ (k\in [i,j)) \]

\[f_{min}[i][j]=min(f_{min}[i][j],f_{min}[i][k]+f_{min}[k+1][j]+s_j-s_{i-1})\ \ \ \ (k\in [i,j)) \]

代碼實現
#include<iostream>
#include<queue>
#include<stack>
#include<cstdio>
#include<cstring>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
const int N=5e2+9;
int a[N];
int n;
int s[N];//前綴和 
int fmax[N][N],fmin[N][N];
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		a[i+n]=a[i]; 
	}
	for(int i=1;i<=n*2;i++)
		s[i]=s[i-1]+a[i]; 
	//memset(fmin,0x3f3f3f,sizeof(fmin));
	for(int l=1;l<n;l++)
		for(int i=1,j=i+l;(i<n+n)&&(j<n+n);i++,j=i+l)
			{
				fmin[i][j]=0x3f3f3f3f;
				for(int k=i;k<j;k++)
				{
					fmax[i][j]=max(fmax[i][j],fmax[i][k]+fmax[k+1][j]+(s[j]-s[i-1]));
					//兩邊i~k和k+1~j,最後一次合併的時候加起來獲得的的分數正好是
					//i~j的和 
					fmin[i][j]=min(fmin[i][j],fmin[i][k]+fmin[k+1][j]+(s[j]-s[i-1]));
				} 
			} 
	int amax=0,amin=0x3f3f3f3f;
	for(int i=1;i<=n;i++)
	{
		amax=max(amax,fmax[i][i+n-1]);
		amin=min(amin,fmin[i][i+n-1]);
	}
	cout<<amin<<endl;
	cout<<amax<<endl;
	return 0;
	return 0;
}

P3146 [USACO16OPEN]248 G

題目描述

(一個由於翻譯而WA的「毒瘤」題)
給定一個長度爲\(n\)的區間,在區間內相鄰的且數字大小相同的兩個數字能夠合併的到一個比它\(+1\)數字
詢問能夠合併成的最大數值爲多少設計

思路分析
  • 一個線性區間dp,咱們依舊是在區間內作處理指針

  • 在一個區間內,枚舉長度,並在這個區間內找一個分割點,是這個點兩邊的數值是相等的,而後進行大小比較code

狀態轉移方程設計
  • 咱們能夠設\(f[i][j]\)爲區間\([i,j]\)內的合併出來的最大值ci

  • 由此能夠獲得狀態轉移方程(狀態能夠根據須要靈活變化,此方程取\(j\)爲分界點)

\[f[i][i+k]=max(f[i][i+k],f[i][j]+1)\ \ (f[i][j]=f[j+1][i+k]) \]

代碼實現
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=5e2+9;
int f[N][N];
int num[N];
int n;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>num[i];
		f[i][i]=num[i];	
	}
	int ans=0;
	for(int k=1;k<=n;k++)//枚舉區間長度 
		for(int i=1;i+k<=n;i++)//確保右端點在範圍內 
			for(int j=i;j<i+k;j++)//保證分割的界限在範圍內 
			{
				if(f[i][j]==f[j+1][i+k])//判斷兩邊是否相等 
				f[i][i+k]=max(f[i][i+k],f[i][j]+1);
				//能夠改爲 f[i][i+k]=max(f[i][i+k],f[j+1][i+k]+1);
				ans=max(f[i][i+k],ans);//在過程當中找答案,節省時間 
			}
	cout<<ans<<endl;
	return 0;
}

P4170 [CQOI2007]塗色

題目描述
  • 一開始給你一個空序列,可使一個字母連續覆蓋相鄰的任意等長的區間,求最少有幾回能夠獲得目標狀態
思路分析
  • 由於是字符串,輸入的時候處理一下讓他的編號從\(1\)開始,好進行處理
  • 首先,每一個長度爲\(1\)的區間都賦值爲\(1\),由於他須要進行一次塗色
  • 其次,咱們能夠發現的是,當咱們枚舉一個區間時,若是左右端點是同樣的,那麼咱們能夠對左右端點分別作操做,比較一下左端點右移\(1\)的區間去覆蓋左端點次數小仍是右端點左移動\(1\)的區間去覆蓋右端點所使用的次數少。
  • 最後進行正常的區間斷點枚舉,知道找出最小的方案爲止
狀態轉移方程設計
  • 正常的狀態轉移方程,設\(f[i][j]\)爲區間\([i,j]\)變成最終狀態所須要的最小次數
  • 那麼能夠獲得狀態轉移方程:

\[f[i][j]=min \begin{cases} f[i+1][j],f[i][j-1]\ \ \ \ (s[i]=s[j]) \\ \\ f[i][j],f[i][k]+f[k+1][j]\ \ \ \ (s[i]!=s[j]) \end{cases}\]

代碼實現
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#include<iostream>
using namespace std;
const int N=1e2+9;
int f[N][N];
char s[N]; 
int main()
{
	cin>>(s+1);
	memset(f,0x3f3f3f3f,sizeof(f));
	for(int i=1;i<=strlen(s+1);i++)
	f[i][i]=1;//開始能夠被塗一次//bingo 
	for(int k=1;k<=strlen(s+1);k++)//枚舉區間 
	{
		for(int i=1;i+k<=strlen(s+1);i++)
		{
			if(s[i]==s[i+k])//兩端點相等,因此咱們就在首次塗色的時候多塗上一層,
			//看看是塗到左邊端點花費少仍是塗到右邊端點花費少
			f[i][i+k]=min(f[i+1][i+k],f[i][i+k-1]); 
			else for(int j=i;j<i+k;j++)
			f[i][i+k]=min(f[i][i+k],f[i][j]+f[j+1][i+k]); 
			//若是兩個端點同樣,那麼就是兩塊區間相加求最小值
			//由於當你左右兩邊不同是,必定會左右均刷一次,
			//而後對於中間的,就看一看是否有同樣的就能夠
			//dp枚舉能夠考慮到上述狀況 
		}
	}
	cout<<f[1][strlen(s+1)]<<endl;
	return 0;
}

P4290 [HAOI2008]玩具取名

思路分析
  • 由於給的每個轉化的字符串是兩個值,因此咱們只須要經過枚舉分別均可以被一個字母表示的兩個小區間,而後看一下這兩個字母是否能夠被一個字母來代替,也就是找一找是否能夠用一個字符來代替整個區間
狀態轉移方程設計
  • \(f[i][j][k]\)表示區間\([i,j]\)能夠經過\(k\)轉化過來
  • \(can[i][j][k]\)表示\(i,j\)能夠經過\(k\)轉化過來(\(z1+z2->z\))

\[f[i][i+k][z]=true\ \ \ (f[i][j][z1]=true,f[j+1][i+k][z2]=true,can[z][z1][z2]=true) \]

代碼實現
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=209; 
bool f[N][N][5],can[5][5][5];//f表示區間[i,j]能夠經過k轉化過來
//can表示i,j能夠經過k轉化過來
int le[5];//存長度
char s[N],c[5];
int ques(char s)
{
	if(s=='W') return 1;
	if(s=='I') return 2;
	if(s=='N') return 3;
	if(s=='G') return 4;
}
int main()
{
	for(int i=1;i<=4;i++) cin>>le[i];
	for(int i=1;i<=4;i++)
	{
		for(int j=1;j<=le[i];j++)
		{
			cin>>c;
			can[i][ques(c[0])][ques(c[1])]=true;//表示i能夠從這倆轉化過來 
		}
	} 
	cin>>(s+1);
	int len=strlen(s+1);
	for(int i=1;i<=len;i++)
	f[i][i][ques(s[i])]=true;
	for(int k=1;k<=len;k++)
		for(int i=1;i+k<=len;i++)
			for(int j=i;j<i+k;j++)
				for(int z=1;z<=4;z++)//枚舉能夠代替z1,z2的數 
					for(int z1=1;z1<=4;z1++)//枚舉z1 
						for(int z2=1;z2<=4;z2++)//枚舉z2 
						{
							if(f[i][j][z1]&&f[j+1][i+k][z2]&&can[z][z1][z2])
							//這個方程表示若是區間[i,j]能夠被z1表示
							//而且區間[j+1,i+k]能夠被z2表示
							//同時z能夠與z1,z2轉化
							//那麼[i,i+k]這個區間就能夠被z來表示 
							f[i][i+k][z]=true;
						}
	bool flag=0;
	if(f[1][len][1]) {flag=1,cout<<'W';};
	if(f[1][len][2]) {flag=1,cout<<'I';};	
	if(f[1][len][3]) {flag=1,cout<<'N';};	
	if(f[1][len][4]) {flag=1,cout<<'G';};
	if(!flag)
	cout<<"The name is wrong!"<<endl;
	return 0;	
}

狀壓DP

P1896 [SCOI2005]互不侵犯

思路分析
  • 首先看一下數據範圍,這是一個狀態壓縮動態規劃,因此咱們就考慮用二進制來進行處理
  • 咱們能夠發現,每個位置的國王數量最可能是\(1\),因此咱們就能夠用一個二進制串串來表示每一行的國王分佈狀況,
  • 一開始咱們能夠預處理一下每一行符合標準的狀態\(situ_{i}\),而後把它這一行的國王數量\(sum_{i}\)和二進制串存到一個數組中,便於枚舉
  • 而後咱們考慮一下這一個狀態如何轉移
  • 按照題目中的條件所說的,每個國王的上下左右及左上下,右上下都不能夠放人!!!
    • $situ_j $ & \(situ_k\) 若是不爲零,說明上下有交叉的
    • $situ_j $ & \(situ_k<<1\),若是不爲零,說明右上放了人
    • \(situ_j<<1\) & \(situ_k\),若是不爲零,說明左上方放了人
  • 這裏還有一個小技巧就是,當咱們枚舉一行的時候,咱們只須要考慮上一行是否符合這個狀態就行了,下一行的狀態能夠轉到下一行的時候在考慮這一行的狀態來判斷便可
狀態轉移方程設計、

\(f[i][j][k]\)表示第\(i\)行,狀態爲第\(i\)行,狀態爲\(j\)時,前\(i\)行的一共放了\(k\)個國王的方案數
獲得如下解題思路

\[f[i][j_1][k]=\sum f[i-1][j_2][k-sum[i]] \]

代碼實現
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#define int long long 
using namespace std;
const int N=11; 
const int M=2009;
int n,num;
int cnt;//狀態的指針 
int situ[M];//可用的狀態 
int sum[M];//求每個狀態所包含的1的數量 
int f[N][(1<<N)][N*N];//表示第i行,狀態是j,放置了k個棋子時的狀態... 
void search(int he,int gs,int pif)//表示狀態,表示1的個數,表示當前爲第幾位 
{
	if(pif>=n)
	{
		situ[++cnt]=he;
		sum[cnt]=gs;
		return;
	}
	search(he,gs,pif+1);//這個就是表示當前位數沒有選,要選擇與他相鄰的位數
	search	(he+(1<<pif),gs+1,pif+2);//當前爲要選的第pif位,因此就在第pif位上標上個1;
	//表示在這個地方有一個國王 
} 
signed main()
{
	cin>>n>>num;
	search(0,0,0);
	for(int i=1;i<=cnt;i++)
		f[1][i][sum[i]]=1; //第二惟爲狀態的下標不是狀態 
	for(int i=2;i<=n;i++)
		for(int j=1;j<=cnt;j++)//枚舉當前的狀態 
			for(int k=1;k<=cnt;k++)//枚舉上一個的狀態 
			{
				if(situ[j]&situ[k])continue;
				if(situ[j]&(situ[k]<<1)) continue;
				if((situ[j]<<1)&situ[k]) continue;
				for(int l=num;l>=sum[j];l--)
				f[i][j][l]+=f[i-1][k][l-sum[j]];
			} 	
	int ans=0;
	for(int i=1;i<=cnt;i++)
	ans+=f[n][i][num];
	cout<<ans<<endl;
	return 0;
}
相關文章
相關標籤/搜索