1. 程式人生 > >動態規劃學習之石子歸併

動態規劃學習之石子歸併

一.題目描述:
在一個圓形操場的四周擺放著N 堆石子(N<=100),現要將石子有次序地合併成一堆.規定每次只能選取
相鄰的兩堆合併成新的一堆,並將新的一堆的石子數記為該次合併的得分.編寫一程式,讀入堆疊數N 及每堆
棧的石子數(<=20).
(1)選擇一種合併石子的方案,使用權得做N-1次合併,得分的總和最小;
(2)選擇一種合併石子的方案,使用權得做N-1次合併,得分的總和最大;
輸入資料:
第一行為石子堆數N;
第二行為每堆的石子數,每兩個數之間用一個空格分隔.
輸出資料:
從第一至第N 行為得分最小的合併方案.第N+1行是空行.從第N+2行到第2N+1行是得分最大合併方案.每
種合併方案用N 行表示,其中第i 行(1<=i<=N)表示第i 次合併前各堆的石子數(依順時針次序輸出,哪一堆先
輸出均可).要求將待合併的兩堆石子數以相應的負數表示.
輸入輸出範例:
輸入:
4
4 5 9 4
輸出:
-4 5 9 -4
-8 -5 9
-13 -9
22
4 -5 -9 4
4 -14 -4
-4 -18
22
二.演算法分析:
競賽中多數選手都不約而同地採用了儘可能逼近目標的貪心法來逐次合併:從最上面的一堆開始,沿順
時針方向排成一個序列.第一次選得分最小(最大)的相鄰兩堆合併,形成新的一堆;接下來,在N-1堆中選得分
最小(最大)的相鄰兩堆合併……,依次類推,直至所有石子經N-1次合併後形成一堆.例如有6堆石子,每堆石子
數依次為3 4 6 5 4 2.要求選擇一種合併石子的方案,使得做5次合併,得分的總和最小.
按照貪心法,合併的過程如下:
每次合併得分:
第一次合併3 4 6 5 4 2 5
第二次合併5 4 6 5 4 9
第三次合併9 6 5 4 9
第四次合併9 6 9 15
第五次合併15 9 24
24
總得分=5+9+9+15+24=62
但是當我們仔細琢磨後,可得出另一個合併石子的方案:
每次合併得分:
第一次合併3 4 6 5 4 2 7
第二次合併7 6 5 4 2 13
第三次合併13 5 4 2 6
第四次合併13 5 6 11
第五次合併13 11 24
24
總得分=7+13+6+11+24=61
顯然,後者比貪心法得出的合併方案更優.題目中的示例故意造成一個貪心法解題的假象,誘使讀者進入“陷
阱”.為了幫助讀者從這個“陷阱”裡走出來,我們先來明確一個問題:
1.最佳合併過程符合最佳原理
使用貪心法之所以可能出錯,是因為每一次選擇得分最小(最大)的相鄰兩堆合併,不一定保證餘下的合
並過程能導致最優解.聰明的讀者馬上會想到一種理想的假設:如果N-1次合併的全域性最優解包含了每一次
合併的子問題的最優解,那麼經這樣的N-1次合併後的得分總和必然是最優的.
例如上例中第五次合併石子數分別為13和11的相鄰兩堆.這兩堆石頭分別由最初的第1,2,3堆(石頭數
分別為3,4,6)和第4,5,6堆(石頭數分別為5,4,2)經4次合併後形成的.於是問題又歸結為如何使得這兩個子序
列的N-2次合併的得分總和最優.為了實現這一目標,我們將第1個序列又一分為二:第1、2堆構成子序列1,第
3堆為子序列2.第一次合併子序列1中的兩堆,得分7;第二次再將之與子序列2的一堆合併,得分13.顯然對於
第1個子序列來說,這樣的合併方案是最優的.同樣,我們將第2個子序列也一分為二:第4堆為子序列1,第5,6堆
構成子序列2.第三次合併子序列2中的2堆,得分6;第四次再將之與子序列1中的一堆合併,得分13.顯然對於
第二個子序列來說,這樣的合併方案也是最優的.由此得出一個結論: 6堆石子經過這樣的5次合併後,得分的
總和最小.
我們把每一次合併劃分為階段,當前階段中計算出的得分和作為狀態,如何在前一次合併的基礎上定義
一個能使目前得分總和最大的合併方案作為一次決策.很顯然,某階段的狀態給定後,則以後各階段的決策不
受這階段以前各段狀態的影響.這種無後效性的性質符最佳原理,因此可以用動態規劃的演算法求解.
2.動態規劃的方向和初值的設定
採用動態規劃求解的關鍵是確定所有石子堆子序列的最佳合併方案.這些石子堆子序列包括:
{第1堆、第2堆}、{第2堆、第3堆}、……、{第N 堆、第1堆};
{第1堆、第2堆、第3堆}、{第2堆、第3堆、第4堆}、……、{第N 堆、第1堆、第2堆}
……
{第1堆、……、第N 堆}、{第2堆、……、第N 堆、第1堆}、……、{第N 堆、第1堆、……、第N-1堆}
為了便於運算,我們用(i,j)表示一個從第i 堆數起,順時針數j 堆時的子序列{第i 堆、第i+1堆、……、第
(i+j-1)mod n 堆}
它的最佳合併方案包括兩個資訊:
①在該子序列的各堆石子合併成一堆的過程中,各次合併得分的總和;
②形成最佳得分和的子序列1和子序列2.由於兩個子序列是相鄰的,因此只需記住子序列1的堆數;
設f(i,j)---將子序列(i,j)中的j 堆石子合併成一堆的最佳得分和;
c(i,j)---將(i,j)一分為二,其中子序列1的堆數; (1≤i≤N,1≤j≤N)
顯然,對每一堆石子來說,它的f(i,1)=0, c(i,1)=0 (1≤i≤N)
對於子序列(i,j) 來說, 若求最小得分總和,f(i,j) 的初始值為∞; 若求最大得分總和,f(i,j) 的初始值為0.
(1≤i≤N,2≤j≤N).
規劃的方向是順推.
先考慮含二堆石子的N 個子序列(各子序列分別從第1堆、第2堆、……、第N 堆數起,順時針數2堆)的合併
方案:
f(1,2), f(2,2), ……, f(N,2)
c(1,2), c(2,2),……, c(N,2)
然後考慮含三堆石子的N 個子序列(各子序列分別從第1堆、第2堆、……、第N 堆數起,順時針數3堆)的合
並方案:
f(1,3), f(2,3), ……, f(N,3)
c(1,3), c(2,3),……, c(N,3)
……
依次類推,直至考慮了含N 堆石子的N 個子序列(各子序列分別從第1堆、第2堆、……、第N 堆數起,順時
針數N 堆)的合併方案:
f(1,N), f(2,N), ……, f(N,N)
c(1,N), c(2,N),……, c(N,N)
最後,在子序列(1,N),(2,N),……,(N,N)中選擇得分總和(f 值)最小(或最大)的一個子序列(i,N)(1≤i≤N),由此出發
倒推合併過程.
3.動態規劃方程和倒推合併過程
對子序列(i,j)最後一次合併,其得分為第i 堆數起,順時針數j 堆的石子總數t.被合併的兩堆石子是由子序
列(i,k)和((i+k-1)modn+1,j-k) (1≤k≤j-1)經有限次合併形成的.為了求出最佳合併方案中的k 值,我們定義一個
動態規劃方程:
當求最大得分總和時
f(i,j)=max{f(i,k)+f(x,j-k)+t} 1≤k≤j-1
c(i,j)=k │f(i,j)=f(i,k)+f(x,j-k)+t 2≤j≤n,1≤i≤n
當求最小得分總和時
f(i,j)=min{f(i,k)+f(x,j-k)+t} 1≤k≤j-1
c(i,j)=k │f(i,j)=f(i,k)+f(x,j-k)+t 2≤j≤n,1≤i≤n
其中x=(i+k-1)modn+1,即第i 堆數起,順時針數k+1堆的堆序號.
例如對上述提到過的6堆石子,按動態規劃方程順推最小得分和.依次得出
含二堆石子的6個子序列的合併方案:
f(1,2)=7 f(2,2)=10 f(3,2)=11 f(4,2)=9 f(5,2)=6 f(6,2)=5
c(1,2)=1 c(2,2)=1 c(3,2)=1 c(4,2)=1 c(5,2)=1 c(6,2)=1
含三堆石子的6個子序列的合併方案:
f(1,3)=20 f(2,3)=25 f(3,3)=24 f(4,3)=17 f(5,3)=14 f(6,3)=14
c(1,3)=2 c(2,3)=2 c(3,3)=1 c(4,3)=1 c(5,3)=1 c(6,3)=2
含四堆石子的6個子序列的合併方案:
f(1,4)=36 f(2,4)=38 f(3,4)=34 f(4,4)=28 f(5,4)=26 f(6,4)=29
c(1,4)=2 c(2,4)=2 c(3,4)=1 c(4,4)=1 c(5,4)=2 c(6,4)=3
含五堆石子的6個子序列的合併方案:
f(1,5)=51 f(2,5)=48 f(3,5)=45 f(4,5)=41 f(5,5)=43 f(6,5)=45
c(1,5)=3 c(2,5)=2 c(3,5)=2 c(4,5)=2 c(5,5)=3 c(6,5)=3
含六堆石子的6個子序列的合併方案:
f(1,6)=61 f(2,6)=62 f(3,6)=61 f(4,6)=61 f(5,6)=61 f(6,6)=62
c(1,6)=3 c(2,6)=2 c(3,6)=2 c(4,6)=3 c(5,6)=4 c(6,6)=3
f(1,6)是f(1,6),f(2,6),……,f(6,6)中的最小值,表明最小得分和是由序列(1,6)經5次合併得出的.我們從這個序
列出發,按下述方法倒推合併過程:
由c(1,6)=3可知,第5次合併的兩堆石子分別由子序列(1,3)和子序列(4,3)經4次合併後得出.其中c(1,3)=2可
知由子序列(1,3)合併成的一堆石子是由子序列(1,2)和第三堆合併而來的.而c(1,2)=1,已表明了子序列(1,2)
的合併方案是第1堆合併第2堆.由此倒推回去,得出第1第2次合併的方案:
每次合併得分
第一次合併3 4 6……7
第二次合併7 6……13
13……
子序列(1,3)經2次合併後合併成1堆,2次合併的得分和=7+13=20.
c(4,3)=1,可知由子序列(4,3)合併成的一堆石子是由第4堆和子序列(5,2)合併而來的.而c(5,2)=1,又表明了子
序列(5,2)的合併方案是第5堆合併第6堆.由此倒推回去,得出第3、第4次合併的方案:
每次合併得分
第三次合併5 4 2……6
第四次合併5 6 ……11
11
子序列(4,3)經2次合併後合併成1堆,2次合併的得分和=6+11=17.
第五次合併是將最後兩堆合併成1堆,該次合併的得分為24.
顯然,上述5次合併的得分總和為最小
20+17+24=61

程式碼實現:


#include<stdio.h>
#include<iostream>
using namespace std;

#define MAX_LONG 0x7fffffff

struct Node
{   // 當前序列的合併方案
 long c;  // 得分和
 int d;  // 子序列的堆數
};

long sumtype[101][101];  // sumtype[i][j] -子序列[i,j]的石子總數
Node list[101][101];  // list[i][j] -子序列[i,j]的合併方案
int date[101],dt[101];  // date[i] -第i 堆石子數,dt - 暫存date
int i,j,N;   // N -石子堆數, i,j - 迴圈變數

void Print(int i,int j)  // 遞迴列印子序列[i,j]的合併過程
{
 int k, x;    // k -迴圈變數,x - 子序列中首堆石子的序號
 if(j != 1)    // 繼續倒推合併過程
 {
  Print(i,list[i][j].d);   // 倒推子序列的合併過程
  x=(i+list[i][j].d-1)%N+1;  // 求子序列中首堆石子的序號
  Print(x,j-list[i][j].d);  // 倒推子序列的合併過程
  for(k=1;k<=N;k++)   // 輸出當前合併第i 堆,第x 堆石子的方案
   if(date[k]>0)
    if(i==k || k==x)
     printf("-%d ",date[k]);
    else
     printf("%d ",date[k]);
  printf("/n");
  date[i]=date[i]+date[x];  // 原第i 堆和第x 堆合併成第i 堆
  date[x]=-date[x];   // 將原第x 堆從圈內去除
 }
}
void solve(int s)
{
 int i,j,k;
 long t,x;
 for(i=1;i<=N;i++)     // 僅含一堆石子的序列不存在合併
 {
  list[i][1].c=0;
  list[i][1].d=0;
 }
 for(j=2;j<=N;j++)     // 順推含堆,含堆……含N 堆石子的各子序列的合併方案
  for(i=1;i<=N;i++)    // 當前考慮從第i 堆數起,順時針數j 堆的子序列
  {
   if(s==1)    // 合併[i,j]子序列的得分和初始化
    list[i][j].c=MAX_LONG;
   else
    list[i][j].c=0;
   t=sumtype[i][j];   // 最後一次合併的得分為[i,j]子序列的石子總數
   for(k=1;k<=j-1;k++)   // 子序列的石子堆數依次考慮堆……j-1堆
   {
    x=(i+k-1)%N+1;   // 求子序列首堆序號
    if((s==1 && list[i][k].c+list[x][j-k].c+t<list[i][j].c) ||
     (s==2 && list[i][k].c+list[x][j-k].c+t>list[i][j].c))
     // 若該合併方案為目前最佳,則記下
    {
     list[i][j].c=list[i][k].c+list[x][j-k].c+t;
     list[i][j].d=k;
    }
   }
  }
  // 在子序列[1,N],[2,N],……,[N, N]中選擇得分總和最小(或最大)的一個子序列
  k=1;
  x=list[1][N].c;
  for(i=2;i<=N;i++)
   if((s==1 && list[i][N].c<x) || (s==2 && list[i][N].c>x))
   {
    k=i;
    x=list[i][N].c;
   }
   Print(k,N);    // 由此出發,倒推合併過程
   printf("%d/n",sumtype[1][N]); // 輸出最後一次將石子合併成一堆的石子總數
   printf("/n");
}
int main()
{
 scanf("%d",&N);    // 讀入石子堆數
 for(i=1;i<=N;i++)
  scanf("%d",&date[i]);  // 讀入每堆石子數
 for(i=1;i<=N;i++)   // 求每一個子序列的石子數sumtype
  sumtype[i][1]=date[i];
 for(j=2;j<=N;j++)
  for(i=1;i<=N;i++)
   sumtype[i][j]=date[i]+sumtype[i%N+1][j-1];
 for(i=1;i<=N;i++)   // 暫存合併前的各堆石子
  dt[i]=date[i];
 solve(1);    // 求得分和最小的合併方案
 for(i=1;i<=N;i++)   // 恢復合併前的各堆石子
  date[i]=dt[i];
 solve(2);    // 求得分和最大的合併方案
 return 0;
}

相關推薦

動態規劃學習石子歸併

一.題目描述:在一個圓形操場的四周擺放著N 堆石子(N<=100),現要將石子有次序地合併成一堆.規定每次只能選取相鄰的兩堆合併成新的一堆,並將新的一堆的石子數記為該次合併的得分.編寫一程式,讀入堆疊數N 及每堆棧的石子數(<=20).(1)選擇一種合併石子的方案

動態規劃專題石子合併

動態規劃專題講義 專題九:合併石子問題 /* Name: 動態規劃專題之石子合併 Author: 巧若拙 Description: 在一個操場上擺放著一排N堆石子。現要將石子有次序地合併成一堆。規定每次只能選相鄰的2堆石子合併成新的一堆,並將新的一堆石子數記為該次合併的得分。 試設計一個

動態規劃練習-環狀石子歸併+四邊形不等式優化

思路:環狀的直接在n後面加上a[0]-a[n]變成鏈狀即可。 這題範圍小,如果n<1000,則必須四邊形不等式優化降低複雜度為O(n^2) Code: #include <bi

動態規劃專題合併石子

/* Name: 動態規劃專題之合併石子 Author: 巧若拙 Description: 在一個操場上擺放著一排N堆石子。現要將石子有次序地合併成一堆。規定每次只能選相鄰的2堆石子合併成新的一堆,並將新的一堆石子數記為該次合併的得分。 試設計一個演算法,計算出將N堆石子合併成一堆的最小得

動態規劃——學習過程01揹包

首先是揹包問題: 1、01揹包   要想計算某一條路徑的值的和的最大值,就需要對每一個節點處的不同值進行分析,取大舍小,然後相加,簡單地來說就是加與不加。 在01揹包問題中,也只有兩種選擇方式,放或者不放。 下面我就先給一個公式:f(i,v)=max{f(i-1,

動態規劃學習筆記

但是 一段 .com mar -m mark 接下來 com 動態 動態規劃學習筆記 20180112 http://www.cnblogs.com/wuyuanyuan/p/8278017.html 這道題雖然補了,但是可能過一段時間又會忘了。接下來一點時間要強化dp的話

動態規劃學習筆記 初識動態規劃

寫在前面 注意:此文章僅供參考,如發現有誤請及時告知。 更新日期:2018/3/16,2018/12/03 動態規劃介紹 動態規劃,簡稱DP(Dynamic Programming) 簡介1 簡介2 動態規劃十分奇妙,它可以變身為記憶化搜尋,變身為遞推,甚至有時可以簡化成一個小小的算式。 動態

動態規劃專題最長公共子序列

動態規劃系列專題講義 專題三:最長公共子序列 /* Name: 動態規劃專題之最長公共子序列 Author: 巧若拙 Description: 1808_公共子序列 描述:我們稱序列Z = < z1, z2, ..., zk >是序列X = < x1, x2,

動態規劃專題最長上升子序列

專題四:最長上升子序列 /*        Name:動態規劃專題之最長上升子序列        Author:巧若拙     &

0基礎學大資料如何系統的規劃學習路?

不知道你是計算機專業應屆生還是已經從業者。總之,有java基礎的學生學習大資料會輕鬆很多,零基礎的小白都需要從java和linux學起。如果你是一個學習能力特別強,而且自律性也很強的人的話可以通過自學。 大資料是當時時代下一門炙熱的IT學科,行情十分火爆,不論是阿里巴巴、百

動態規劃演算法零一揹包問題

 問題描述 現在有一個揹包,這個揹包的容積一定 但是現在有n個物品,每個物品都有對應的體積和價值 要求如何讓操作才能夠使得我們講儘可能值錢的物品放到這個揹包裡面  解題思路與演算法思想 當看到這道題的時候,我們很自然地會認為這是一個搜尋問題

C++記憶化搜尋演算法與動態規劃演算法公共子序列

公共子序列 Description 我們稱序列Z = < z1, z2, ..., zk >是序列X = < x1, x2, ..., xm >的子序列當且僅當存在 嚴格上

C++動態規劃演算法Maximum sum(最大和)

Maximum sum(最大和) Description Given a set of n integers: A={a1, a2,..., an}, we define a function d

【初學動態規劃裝箱問題

裝箱問題就是揹包問題的簡化版……就是給出一個容量v,然後給出n個物品的重量,把物品裝進箱子裡,求箱子的最小剩餘容量。 #include <iostream> using namespace std; int w[101]; bool ans[101]={fa

動態規劃入門國王的金礦

最近學習演算法,對動態規劃不太瞭解,使用的時候照搬轉移方程式,知其然不知其所以然,今天看到一篇動態規劃的教程,解釋得非常通俗,原文在這裡[動態規劃入門教程] (http://blog.csdn.net/woshioosm/article/details/74

動態規劃問題 鋼條切割

動態規劃與分治演算法異同: 分治演算法將問題劃分為互不相交的子問題,遞迴的求解子問題。分治演算法會做出許多不必要的工作,會反覆求解那些公共子問題。而動態規劃對子問題只求解一次,將其儲存在一個表格裡面,無需每次都重新計算。 動態規劃通常用來求解最優化問題,這個

動態規劃思想最小硬幣分配數

動態規劃演算法,第一次接觸大概也是一年多前了,那會為了參加個ACM競賽,倉促看了下概念,之後由於去搞各種開發又對演算法不了了之了。最近深感自己內功的薄弱,準備再次進軍下演算法部分,今天就以一個簡單的OJ題練練手,好好體會下DP思想,並留下點感想,可以給自己日後回

0-1揹包優化動態規劃演算法跳躍點法

// 動態規劃 揹包問題 跳躍點優化 #include <iostream> using namespace std; template<class Type> void Traceback(int n,Type w[],Type v[]

動態規劃學習-【0-1揹包問題】

題目描述: 問題:0-1揹包問題 描述:有一個揹包,它的容量為C(Capacity),現在有n種不同的物品編號分別為0...n-1,其中每一件物品的重量為w(i),價值為v(i)。問可以向這個揹包中盛放哪些物品,使得在不超過揹包容量的基礎上,物品的總價值最大。 解題思

動態規劃演算法尋找最長迴文數串

       給定一個字串s,你可以從中刪除一些字元,使得剩下的串是一個迴文串。如何刪除才能使得迴文串最長呢? 輸出需要刪除的字元個數。 本題可轉化為動態規劃演算法求解最長公共子序列問題,然後用總字串