动态规划技巧专题
動(dòng)態(tài)規(guī)劃技巧
- 一、0,1背包問(wèn)題
- 記憶化搜索
- 迭代法
- 0,1背包問(wèn)題(數(shù)據(jù)大)
- 計(jì)算第K個(gè)答案—摩爾斯電碼字典()
- DFS 求出生成的所有信號(hào)
- 利用動(dòng)態(tài)規(guī)劃求解
- 龍曲線
- DFS 求出進(jìn)化后的字符串
- 動(dòng)態(tài)規(guī)劃求解
- 旅行商問(wèn)題
- 小技巧
- 記憶化搜索
- 韋布巴津
- 搜索所有可能
- 動(dòng)態(tài)規(guī)劃
- 組合游戲
- 數(shù)字游戲
- 方塊游戲
- 完全背包問(wèn)題
- 硬幣問(wèn)題
- 得到金額w 需要的最小的硬幣個(gè)數(shù)
- 得到金額 W 的所有方案?jìng)€(gè)數(shù)
一、0,1背包問(wèn)題
一、問(wèn)題描述:
有n 個(gè)物品,它們有各自的重量和價(jià)值,現(xiàn)有給定容量的背包,如何讓背包里裝入的物品具有最大的價(jià)值總和?
解題思路:對(duì)于每一個(gè)轉(zhuǎn)態(tài),只有選擇和不選擇兩中選擇。
記憶化搜索
int n,capacity; int volume[100],need[100]; int cache[1001][1001]; string name[100];//背包剩余空間為capacity 時(shí),將返回放入item以后的物品 //所能達(dá)到的最大值 int pack(int capacity,int item){if(item == n)return 0;int &ret = cache[capacity][item];if(ret != -1)return ret;//不放入物品的情況ret = pack(capacity,item+1);//不放入物品的情況if(capacity>=volume[item])ret = max(ret,pack(capacity-volume[item],item+1)+need[item]);return ret; }迭代法
dp[i][j] 的含義是在j容量下放入i件物品的最大價(jià)值。在遍歷容量的時(shí)候記得是從后向前遍歷的,否則結(jié)果不會(huì)有變化。
#include <iostream>using namespace std;int w[105], val[105]; int dp[105][1005];int main() {int t, m, res=-1;cin >> t >> m;for(int i=1; i<=m; i++)cin >> 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]; }cout << dp[m][t] << endl;return 0; }0,1背包問(wèn)題(數(shù)據(jù)大)
有n個(gè)重量和價(jià)值分別為wi, vi的物品,從這些物品中挑選出總重量不超過(guò)W的物品,求所有挑選方案中價(jià)值的最大值
與前面唯一不同的地方便是W達(dá)到了10 ^ 9、wi到達(dá)1了10 ^ 7;
變換思路:定義dp[i+1][j]為前i個(gè)物品中挑選出價(jià)值總和為j時(shí)總重量的最小值(不存在時(shí)就是一個(gè)充分大的數(shù)INF)。
因?yàn)榍?個(gè)物品什么也選擇不了、故
dp[0][0] = 0;
dp[0][j]=INF;
此外 前i個(gè)物品挑選出價(jià)值總和為j時(shí),一定有
前i-1個(gè)物品中挑選價(jià)值總和為j的部分
前i-1個(gè)物品中挑選價(jià)值總和為j-v[i]的部分,然后在選中第i個(gè)物品
于是又遞推式dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]] + w[i])
最終的答案就對(duì)應(yīng)于dp[n][j] <= W的最大的j;
計(jì)算第K個(gè)答案—摩爾斯電碼字典()
題目
摩爾斯電碼字典
在沒(méi)有電話的時(shí)代,摩爾斯電碼是無(wú)線電傳輸領(lǐng)域中的一種常用代碼。電碼以短信號(hào)(短點(diǎn),o)和長(zhǎng)信號(hào)(長(zhǎng)點(diǎn),-)的不同組合表示各種文字。例如:o—表示英文字母J,而—表示英文字母M。
假設(shè)有一本以n個(gè)長(zhǎng)點(diǎn)和m(n、m<=100)個(gè)短點(diǎn)組成的、包含所有信號(hào)的字典。例如:n=m=2,就會(huì)包含如下信號(hào)。
–oo
-o-o
-oo-
o–o
o-o-
oo–
這些信號(hào)已按照字典順序排列好了。-的ASKII碼是45,而o的ASCII碼是111。因此,按照字典順序,-在前,o在后。給定n和m時(shí),編寫代碼計(jì)算出此字典的第k(k<=1,000,000,000,000)個(gè)信號(hào)。例如:上述字典的第四個(gè)信號(hào)是o–o。
DFS 求出生成的所有信號(hào)
// s 已經(jīng)生成的信號(hào) //n:還需要的 - 的個(gè)數(shù) // m:還需要的 o 的個(gè)數(shù)void generate(int n,int m,string s){if(n == 0|| m == 0){cout<<s<<endl;return ;}if(n>0)generate(n-1,m,s+"-");if(m>0)generate(n,m-1,s+"o");}利用動(dòng)態(tài)規(guī)劃求解
解題思路:首先要了解把n個(gè)長(zhǎng)點(diǎn)和m個(gè)端點(diǎn)連到s后面,可以用二項(xiàng)式系數(shù)表示這種組合的個(gè)數(shù),即(Cn+mm)(C^m_{n+m})(Cn+mm?)
第一個(gè)字符是長(zhǎng)點(diǎn)的信號(hào)會(huì)有多少個(gè)呢?第一個(gè)是長(zhǎng)點(diǎn)或,還有n-1個(gè)長(zhǎng)點(diǎn)和m個(gè)短點(diǎn)。因此有Cn+m?1n?1個(gè)C^{n-1}_{n+m-1}個(gè)Cn+m?1n?1?個(gè)
龍曲線
龍曲線介紹:
龍曲線是以簡(jiǎn)單的數(shù)學(xué)規(guī)則畫出一種曲線,它具有以下形態(tài)。曲線從一個(gè)簡(jiǎn)單的線段起始,按照一定規(guī)則變換此線段完成整個(gè)曲線。每形成一次變換稱為“完成了一次變換代”,而每完成一代,曲線會(huì)進(jìn)化到更復(fù)雜的形式。像這種“放大其一小部分的形狀時(shí),表現(xiàn)出與整個(gè)形狀極為相似構(gòu)造的圖形”,就是分形。
畫出龍曲線的方法暫且就稱為龍曲線字符串吧!龍曲線字符串由X、Y、F、+、-組成。
那么,要畫出龍曲線就從一個(gè)點(diǎn)起始畫出如下曲線即可。
F:向前方移動(dòng)一格并畫線。
+:向左旋轉(zhuǎn)90度。
-:向右旋轉(zhuǎn)90度。
X、Y:忽略。
畫出第0代龍曲線的字符串是FX。從下一代開(kāi)始,按照如下方式利用前一代字符串進(jìn)行字符替換,從而獲得當(dāng)前一代的龍曲線字符串。
X-> X+YF
Y-> FX+Y
根據(jù)上面的替換式,就有如下的1、2代龍曲線字符串。
第一代:FX+YF
第二代:FX+YF+FX-YF
我們想要求出第n代龍曲線字符串。不過(guò),考慮到答案有可能很長(zhǎng),所以只想計(jì)算出第p個(gè)字符起始長(zhǎng)度為l個(gè)字符的字符串。請(qǐng)編寫程序?qū)崿F(xiàn)這種功能。
輸入
第一行輸入測(cè)試用例的個(gè)數(shù)C(C<=50)。各測(cè)試用例的第一行分別輸入3個(gè)整數(shù),即龍曲線的世代n(0<=n<=50)、p以及l(fā)(1<=p<=1 000 000 000、1<=l<=50)。第n代龍曲線字符串的長(zhǎng)度可假設(shè)成總是大于等于p+l的數(shù)值。
輸出
每個(gè)測(cè)試用例在1行內(nèi)輸出第n代龍曲線字符串的第p個(gè)字符開(kāi)始,輸出l個(gè)字符。
示例輸入
4
0 1 2
1 1 5
2 6 5
42 764853475 30
示例輸出
FX
FY+YF
+FX-Y
FX-YF-FX+YF+FX-YF-FX+YF-FX-YF-
算法的詳細(xì)設(shè)計(jì)思想:
分別找出字母和“+”“-”號(hào)規(guī)律,對(duì)其思想分別進(jìn)行函數(shù)構(gòu)造,最后進(jìn)行整合,龍曲線字符串每一代都是前一代的前半部分,代數(shù)只決定了龍曲線字符串長(zhǎng)度或者圖形的終止位,但此次要求不輸出圖形只輸出字符串規(guī)律,也就是說(shuō)所有代數(shù)字符串長(zhǎng)度內(nèi)(例如第n代長(zhǎng)度為3*2^n-1),相同位置規(guī)律是相同的。
由題目可推得:
第一代:FX+YF
第二代:FX+YF+FX-YF
第三代:FX+YF+FX-YF+FX+YF-FX-YF
由上式可看出每一代都是下一代前半部分,還可推出兩個(gè)規(guī)律,字母規(guī)律和正負(fù)號(hào)規(guī)律。
字母規(guī)律:每六個(gè)一循環(huán),第一,二,四,五位分別為F,X,Y,F.
符號(hào)規(guī)律:符號(hào)位都是3的倍數(shù),并且規(guī)律如下。
第一代 +
第二代 + + -
第三代 + + - + + - -
1 2 3 4 5 6 7
由上式可看出,每代符號(hào)會(huì)繼承上一代,并且在它們空隙處按照“+” “-”循環(huán)插入,類如,第三代繼承第二代的2,4,6位置,在1,3,5,7按照“+”“-”循環(huán)插入。由此我們可以推算出在奇數(shù)位上1,3,5,7······上的奇數(shù)位是“+”,偶數(shù)位是“-”。例如7在奇數(shù)中排第
DFS 求出進(jìn)化后的字符串
curve(seed,generations) = 輸出初始化字符串進(jìn)化generations代之后的結(jié)果。進(jìn)化就是對(duì)字符中的原字符進(jìn)行替換。
代碼:
void curve(const string &seed,int generations){if(generations == 0){cout<<seed<<endl;return;}for(int i=0;i<seed.size();++i){if(seed[i] =='X')curve("X+YF",generations-1);else if(seed[i] == 'Y')curve("FX-Y",generations-1);elsecout<<seed[i];} }動(dòng)態(tài)規(guī)劃求解
因?yàn)?已經(jīng)定義了X和Y 替換的字符有xLength(n)=xLength(n?1)+yLength(n?1)+2xLength(n) = xLength(n-1) +yLength(n-1)+2xLength(n)=xLength(n?1)+yLength(n?1)+2 yLength(n)=xLength(n?1)+yLength(n?1)+2yLength(n) = xLength(n-1) +yLength(n-1)+2 yLength(n)=xLength(n?1)+yLength(n?1)+2
所以有
Length(n)=2+2?Length(n?1)Length(n) = 2+ 2*Length(n-1)Length(n)=2+2?Length(n?1)
代碼:
const int MAX = 1e9+1; // Length[i] = 把X或Y替換i次后的長(zhǎng)度 int Length[51];void precalc(){Length[0] = 1;for(int i=1;i<=50;++i)Length[i] = min(MAX,Length[i]*2+2); }const string EXPAND_x = "X+YF"; const string EXPAND_Y = "FX=Y"; //返回dragonCurve 進(jìn)化 generations 代后的第 skip+1 個(gè)字符 char expand(const string& dragonCurve,int generations,int skip){if(generations == 0){assert(skip <dragonCurve.size());return dragonCurve[skip];}for(int i=0;i<dragonCurve.size();++i){//字符串?dāng)U展時(shí)if(dragonCurve[i] =='X' || dragonCurve[i] == 'Y'){if(skip >= Length[generations])skip -= Length[generations];else if(dragonCurve[i] == 'X')return expand(EXPAND_x,generations-1,skip);else return expand(EXPAND_Y,generations-1,skip);}else if(skip>0) // +,--符號(hào)--skip;else return dragonCurve[i];}//不擴(kuò)展但需要跳過(guò)}旅行商問(wèn)題
問(wèn)題簡(jiǎn)介
一個(gè)商品推銷員要去若干個(gè)城市推銷商品,該推銷員從一個(gè)城市出發(fā),需要經(jīng)過(guò)所有城市后,回到出發(fā)地。應(yīng)如何選擇行進(jìn)路線,以使總的行程最短。從圖論的角度來(lái)看,該問(wèn)題實(shí)質(zhì)是在一個(gè)帶權(quán)完全無(wú)向圖中,找一個(gè)權(quán)值最小的Hamilton回路。由于該問(wèn)題的可行解是所有頂點(diǎn)的全排列,隨著頂點(diǎn)數(shù)的增加,會(huì)產(chǎn)生組合爆炸,它是一個(gè)NP完全問(wèn)題。
小技巧
//輸入值是布爾類型數(shù)組
當(dāng)數(shù)組的長(zhǎng)度為n時(shí),可能的輸入值個(gè)數(shù)是2n2^n2n。因此,可以把長(zhǎng)度為n的數(shù)組解釋成長(zhǎng)度為n的二進(jìn)制數(shù)。等同于A【】中的A【0】是最低位,A【1】是其上一位,A【n-1】是最高位的二進(jìn)制。
記憶化搜索
為了適用記憶化的方法、應(yīng)該把函數(shù)的定義修改成“盡可能最小限度地接收已選路徑方面的倍息”。共有兩處使用已選路徑path;完成整個(gè)路徑時(shí)計(jì)算整個(gè)路徑長(zhǎng)度,查看是否已經(jīng)訪問(wèn)過(guò)某個(gè)城市。那么、把定義修改成如下形式就能少接收信息。
- 1.計(jì)算已訪問(wèn)路徑的長(zhǎng)度:原先shortest Path(函數(shù)返回整個(gè)路徑長(zhǎng)度,將其修改成返回剩余路徑的最小長(zhǎng)度。修改成這種形式后就不需要知道之前各城市的訪問(wèn)順序,但需要知道當(dāng)前位置(已選擇路線中的最后一個(gè)城市)。
- 2.查看是否已經(jīng)訪問(wèn)過(guò)某個(gè)城市:必須查看是否已經(jīng)訪問(wèn)過(guò)某城市,但訪問(wèn)順序并不重要。因此,只需傳遞給函數(shù)長(zhǎng)度為n的布爾型數(shù)組即可。
通過(guò)這些修改可定義出如下函數(shù)。
shortestPath2(here,visited)=給定當(dāng)前位置here,以及各個(gè)城市是否被訪問(wèn)過(guò)的信息,并保存在布爾型數(shù)組visited時(shí),從here起始訪問(wèn)剩余城市的子路徑中,返回長(zhǎng)度最小的路徑。.
代碼:
int n,dist[MAX][MAX]; double cache[MAX][1<<MAX];//初始化為-1 // here :當(dāng)前位置 //visited : 表示是否已訪問(wèn)各城市 //從here起訪問(wèn)剩余城市的路徑中,返回長(zhǎng)度最小的路徑。 // 訪問(wèn)剩余所有城市的路徑中,返回長(zhǎng)度最小的路徑 //假設(shè)總是從0號(hào)城市開(kāi)始double shortPath(int here,int visited){if(visited == (1<<n)-1)return dist[here][0];//制表double &ret = cache[here][visited];if(ret>=0)return ret;ret = INF;//嘗試所有要訪問(wèn)的下一個(gè)城市for(int next =0;next<n;++next){//已經(jīng)訪問(wèn)過(guò)的城市if(visited &(1<<next))continue;double cand = dist[here][next] + shortPath(next,visited+(1<<next));ret = min(ret,cand);}return ret; }韋布巴津
題意:
雞蛋的價(jià)格要滿足一下三個(gè)條件
搜索所有可能
前一個(gè)位置滿足以下三個(gè)條件才能使用當(dāng)前位置的數(shù)字。
代碼:
//digits :對(duì)e的各位數(shù)字進(jìn)行排序后的結(jié)果 string e,digits; int n,m; //輸出所有以e的各位數(shù)字組成的結(jié)果 //price :已生成的價(jià)格 //taken :各位數(shù)字是否已被使用void generate(string price,bool taken[15]){if(price.size() == n){if(price <e) // 雖然是字符串,但當(dāng)位數(shù)一樣時(shí)也可以利用<比較大小cout<<price<<endl;return ;}for(int i=0;i<n;++i){if(!taken[i]&&(i == 0||digits[i-1]!=digits[i]||taken[i-1])){taken[i] = true;generate(price + digits[i],taken);taken[i] = false;}} }動(dòng)態(tài)規(guī)劃
是否是整除,如果都能整除,只要余數(shù)相同,相減之前的數(shù)并不長(zhǎng)重要。也就是一個(gè)小推論,如果兩個(gè)數(shù)相減能整除m,那么這兩個(gè)數(shù)mod m 的余數(shù)是一樣的。
int MOD = 1e9+10; //digits :對(duì)e的各位數(shù)字進(jìn)行排序后的結(jié)果 string e,digits; int n,m; int cache[1<<14][20][2]; //從第一位開(kāi)始對(duì)過(guò)去的價(jià)格進(jìn)行逐個(gè)相加 //index :當(dāng)前位置的小標(biāo) //taken :已使用位置的集合 //mod : 目前為止生成的價(jià)格除以m的余數(shù) //less :目前為止生成的價(jià)格如果小于e取1,否則為0int price(int index,int taken,int mod,int less){if(taken == n)return (less && mod == 0)?1:0;// 制表int & ret = cache[taken][mod][less];if(ret!=-1)return ret;ret = 0;for(int next =0;next<n;++next){if((taken&(1<<next)) == 0){ // 當(dāng)前位置沒(méi)用過(guò)//過(guò)去的價(jià)格必須小于新的價(jià)格if(!less && e[index]<digits[next])continue;//相同數(shù)字只能用一次if(next >0&&digits[next-1] == digits[next] &&(taken &(1<<next-1)) == 0)continue;int nextTaken = taken|(1<<next); // 把taken 中第 next 位置為1int nextMod = (mod *10 + digits[next]-'0')%m; int nextLess = less|| e[index] > digits[next];ret += price(index+1,nextTaken,nextMod,nextLess);ret %= MOD;}}return ret; }組合游戲
數(shù)字游戲
題目大意:
賢宇和舒夏正在用n個(gè)整數(shù)排成一行的棋盤做游戲。游戲從賢宇開(kāi)始,雙方輪流進(jìn)行。每個(gè)人輪到自己要走棋的時(shí)候,能夠做出如下兩種選擇。
- 可以拿走棋盤最左或最右的一個(gè)數(shù)字,被拿走的數(shù)字會(huì)從棋盤中抹掉。
- 棋盤中還剩余兩個(gè)以上的數(shù)字時(shí),可以把棋盤最右或最左的兩個(gè)數(shù)字抹掉。
棋盤上的所有數(shù)字都消失后就結(jié)束游戲,每個(gè)人的分?jǐn)?shù)是自己手中數(shù)字之和。兩人按照“每相差1分就付給分?jǐn)?shù)高的人100韓元”的方式投注,他們都全力以赴的時(shí)候,最終的分?jǐn)?shù)差會(huì)是多少呢?
方塊游戲
題目大意:
鎮(zhèn)浩和賢煥厭倦了模擬城市和寶石迷陣游戲,打算利用家里的組合型方塊進(jìn)行一個(gè)新的游戲。雙方輪流向5×5大小的棋盤里放置組合型方塊,這些方塊已經(jīng)組合成L形狀的三方塊組和一字型的兩方塊組。放置時(shí),必須與棋盤中的分割線對(duì)齊,而且不能重疊。下圖是進(jìn)行游戲時(shí)的棋面布局。
如圖所示,各個(gè)方塊組可以翻轉(zhuǎn)或旋轉(zhuǎn)。兩名棋手輪流放置方塊組,到不能再放置時(shí),最后一個(gè)放置的棋手將會(huì)取得勝利。編寫一個(gè)程序,給出一種棋面布局時(shí),使之判斷當(dāng)前走棋的運(yùn)行有沒(méi)有取勝的方法。
實(shí)現(xiàn)方法:
它利用了第16章介紹的位掩碼使代碼變得更加簡(jiǎn)單。傳遞給play0的數(shù)值并不是長(zhǎng)度為25的布爾類型數(shù)組,而是32位的整數(shù)型。要想在看(c、)格子中有無(wú)方塊,只要訪問(wèn)第y×5+x位并查看其數(shù)值就能判斷。接下來(lái),利用位梯碼就能夠輕松判斷出當(dāng)前組合塊能否放置到各個(gè)格子中。假如想把二方塊組橫向放置到(0,0)和(0,1)格子當(dāng)中,那么第0和T位需要變成1,所以用數(shù)值表示此方塊組占用的位數(shù)就是3。代碼9-22也采用了這種方法,算法預(yù)先按照占用的位數(shù)計(jì)算出各個(gè)組合塊能夠擺出的布局,之后與當(dāng)前棋面布局進(jìn)行位AND運(yùn)算,由此判斷當(dāng)前組合塊能否放到此位置。precalc()會(huì)生成組合塊的所有可能的組合,然后把所有可能組合占用位數(shù)的集合保存到moves[。此時(shí),playO內(nèi)部會(huì)對(duì)此結(jié)果進(jìn)行循環(huán)檢索,與當(dāng)前的棋面布局進(jìn)行位AND運(yùn)算。如果得到的答案是0,就表示可以放置當(dāng)前組合塊.
代碼:
vector<int>moves;inline int cell(int y,int x){return 1<<(y*5+x); } //預(yù)先計(jì)算出能夠放置到棋盤上的各組合快的位置 void precalc(){//計(jì)算三方塊組成的L形組合快for(int y = 0;y<4;++y)for(int x = 0;x<4;++x){vector<int>cells;for(int dy = 0;dy<2;++dy)for(int dx = 0;dx<2;++dx)cells.push_back(cell(y+dy,x+dx));int square = cells[0] + cells[1] + cells[2] + cells[3];for(int i=0;i<4;++i)moves.push_back(square-cells[i]);}//計(jì)算兩方塊組成的組合快for(int i=0;i<5;++i)for(int j=0;j<4;++j){moves.push_back(cell(i,j)+cell(i,j+1));moves.push_back(cell(j,i)+cell(j+1,i));} }char cache[1<<25]; //當(dāng)前棋面布局為board 時(shí),返回騎手能否取勝 char play(int board){char &ret = cache[board];if(ret != -1)return ret;ret = 0;// 考慮所以走法for(int i=0;i<moves.size();++i)// 確定此走法能否適用于當(dāng)前的棋面布局if((moves[i]&board) == 0){if(!play(board|moves[i])){ret = 1;break;}}return ret; }完全背包問(wèn)題
和0,1背包很像,不過(guò)選取的物品總量上限是無(wú)限大的。
C[i] 的含義是 容量為 budget 時(shí)所得的最大價(jià)值價(jià)值
都是從 低到高,不像0,1。容量是從高到低
硬幣問(wèn)題
得到金額w 需要的最小的硬幣個(gè)數(shù)
int n,m; int dp[N],a[25]; int main() {while(~scanf("%d%d",&n,&m)){for(int i=0;i<= n;++i)dp[i] = INF;for(int i=0;i<m;++i)scanf("%d",&a[i]);dp[0] = 0;for(int i=0;i<m;++i)for(int j=1;j<=n;++j)if(j>=a[i])dp[j] = min(dp[j] , dp[j-a[i]]+1);printf("%d\n",dp[n]);}return 0; }得到金額 W 的所有方案?jìng)€(gè)數(shù)
dp[i] 的含義是 得到容量 i 有 dp[i] 種方法
dp[0] = 1; for(int i = 0;i<n;++i) for(int j=1;j<=amount;++j) if(j - coins[i] >= 0) dp[j] = dp[j] + dp[j-coins[i]];總結(jié)
- 上一篇: 最长递增子序列问题合集
- 下一篇: 贪心专题技巧