【算法】分治算法
分治算法:
- 一、基本概念
- 二、基本思想及策略
- 三、分治法使用場景
-
- 五、分治法的復雜性分析
- 六、可使用分治法求解的一些經(jīng)典問題
- (1)二分搜索
- (2)大整數(shù)乘法
- (3)Strassen矩陣乘法
- (4)棋盤覆蓋
- (5)合并排序
- (6)快速排序
- (7)線性時間選擇
- (8)最接近點對問題
- (9)循環(huán)賽日程表
- (10)漢諾塔詳解
-
- 漢諾塔公式:ans=2^n^-1
- 牛牛的漢諾塔
)
一、基本概念
在計算機科學中,分治法是一種很重要的算法。字面上的解釋是“分而治之”,就是把一個復雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合并。這個技巧是很多高效算法的基礎,如排序算法(快速排序,歸并排序),傅立葉變換(快速傅立葉變換)……
任何一個可以用計算機求解的問題所需的計算時間都與其規(guī)模有關。問題的規(guī)模越小,越容易直接求解,解題所需的計算時間也越少。例如,對于n個元素的排序問題,當n=1時,不需任何計算。n=2時,只要作一次比較即可排好序。n=3時只要作3次比較即可,…。而當n較大時,問題就不那么容易處理了。要想直接解決一個規(guī)模較大的問題,有時是相當困難的。
二、基本思想及策略
分治法的設計思想是:將一個難以直接解決的大問題,分割成一些規(guī)模較小的相同問題,以便各個擊破,分而治之。
分治策略是:對于一個規(guī)模為n的問題,若該問題可以容易地解決(比如說規(guī)模n較小)則直接解決,否則將其分解為k個規(guī)模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解這些子問題,然后將各子問題的解合并得到原問題的解。這種算法設計策略叫做分治法。
如果原問題可分割成k個子問題,1<k≤n,且這些子問題都可解并可利用這些子問題的解求出原問題的解,那么這種分治法就是可行的。由分治法產(chǎn)生的子問題往往是原問題的較小模式,這就為使用遞歸技術提供了方便。在這種情況下,反復應用分治手段,可以使子問題與原問題類型一致而其規(guī)模卻不斷縮小,最終使子問題縮小到很容易直接求出其解。這自然導致遞歸過程的產(chǎn)生。分治與遞歸像一對孿生兄弟,經(jīng)常同時應用在算法設計之中,并由此產(chǎn)生許多高效算法。
三、分治法使用場景
分治法所能解決的問題一般具有以下幾個特征:
1) 該問題的規(guī)模縮小到一定的程度就可以容易地解決
2) 該問題可以分解為若干個規(guī)模較小的相同問題,即該問題具有最優(yōu)子結構性質(zhì)。
3) 利用該問題分解出的子問題的解可以合并為該問題的解;
4) 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。
第一條特征是絕大多數(shù)問題都可以滿足的,因為問題的計算復雜性一般是隨著問題規(guī)模的增加而增加;
第二條特征是應用分治法的前提它也是大多數(shù)問題可以滿足的,此特征反映了遞歸思想的應用;、
第三條特征是關鍵,能否利用分治法完全取決于問題是否具有第三條特征,如果具備了第一條和第二條特征,而不具備第三條特征,則可以考慮用貪心法或動態(tài)規(guī)劃法。
第四條特征涉及到分治法的效率,如果各子問題是不獨立的則分治法要做許多不必要的工作,重復地解公共的子問題,此時雖然可用分治法,但一般用動態(tài)規(guī)劃法較好。
四、分治法得基本步驟
分治法在每一層遞歸上都有三個步驟:
step1 分解:將原問題分解為若干個規(guī)模較小,相互獨立,與原問題形式相同的子問題;
step2 解決:若子問題規(guī)模較小而容易被解決則直接解,否則遞歸地解各個子問題
step3 合并:將各個子問題的解合并為原問題的解。
它的一般的算法設計模式如下:
Divide-and-Conquer§
1. if |P|≤n0
2. then return(ADHOC§)
3. 將P分解為較小的子問題 P1 ,P2 ,…,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 遞歸解決Pi
6. T ← MERGE(y1,y2,…,yk) △ 合并子問題
7. return(T)
其中|P|表示問題P的規(guī)模;n0為一閾值,表示當問題P的規(guī)模不超過n0時,問題已容易直接解出,不必再繼續(xù)分解。ADHOC§是該分治法中的基本子算法,用于直接解小規(guī)模的問題P。因此,當P的規(guī)模不超過n0時直接用算法ADHOC§求解。算法MERGE(y1,y2,…,yk)是該分治法中的合并子算法,用于將P的子問題P1 ,P2 ,…,Pk的相應的解y1,y2,…,yk合并為P的解。
五、分治法的復雜性分析
一個分治法將規(guī)模為n的問題分成k個規(guī)模為n/m的子問題去解。設分解閥值n0=1,且adhoc解規(guī)模為1的問題耗費1個單位時間。再設將原問題分解為k個子問題以及用merge將k個子問題的解合并為原問題的解需用f(n)個單位時間。用T(n)表示該分治法解規(guī)模為|P|=n的問題所需的計算時間,則有:
T(n)= k T(n/m)+f(n)
通過迭代法求得方程的解:
遞歸方程及其解只給出n等于m的方冪時T(n)的值,但是如果認為T(n)足夠平滑,那么由n等于m的方冪時T(n)的值可以估計T(n)的增長速度。通常假定T(n)是單調(diào)上升的,從而當mi≤n<mi+1時,T(mi)≤T(n)<T(mi+1)。
六、可使用分治法求解的一些經(jīng)典問題
(1)二分搜索
二分搜索
(2)大整數(shù)乘法
(3)Strassen矩陣乘法
(4)棋盤覆蓋
(5)合并排序
(6)快速排序
(7)線性時間選擇
(8)最接近點對問題
(9)循環(huán)賽日程表
(10)漢諾塔詳解
在漢諾塔游戲中,有三個分別命名為A、B、C得塔座,幾個大小各不相同,從小到大一次編號得圓盤,每個原盤中間有一個小孔。最初,所有得圓盤都在A塔座上,其中最大得圓盤在最下面,然后是第二大,以此類推.
游戲的目的是將所有的圓盤從塔座A移動到塔座B;塔座C用來防止臨時圓盤,游戲的規(guī)則如下:
1、一次只能移動一個圓盤
2、任何時候都不能將一個較大的圓盤壓在較小的圓盤上面.
3、除了第二條限制,任何塔座的最上面的圓盤都可以移動到其他塔座上.
漢諾塔問題解決思想:
在解決漢諾塔問題時,事實上,我們不是關心圓盤1開始應該挪到哪個塔座上,而是關心最下面的圓盤4.當然,我們不能直接移動圓盤4,但是圓盤4最終將從塔座A移動到塔座B.按照游戲規(guī)則,在移動圓盤4之前的情況一定如下圖
我們?nèi)詫⒎治?如何將前三個圓盤從A移動到C,然后圓盤4從A移動到B,前三個圓盤從C再移動到B.
但是上面的步驟可以重復利用!例如將三個圓盤從A移動到C,那么應該先將前兩個圓盤從A移動到B,然后將圓盤3從A移動到C,最后將前兩個圓盤從B移動到C.
持續(xù)簡化這個問題,最終我們將只需要處理一個圓盤從一個塔座移動到另一個塔座的問題.
我們先來想一下我們?nèi)祟悜撛鯓硬僮靼伞?/p>
我們每次操作都會這樣問自己:我們需要將哪個柱子上的多少個盤子通過哪個柱子移動到哪個柱子上呢?
我們必須也只能用這么幾個參數(shù):
需要移動的盤子的總數(shù),3個柱子。
所以函數(shù)頭為:
void hanoi(int n, char x, char y, char z)
其中,n代表盤子總數(shù),x,y,z代表柱子
hanoi(n, x, y, z)的意思就是:將n個在x柱子上的盤子通過y這個柱子移動到z這個柱子上。
那不就完了!
hanoi(n, ‘A’, ‘B’, ‘C’)就是這道問題的答案!
那么這一步的前一步是什么?
記住了,在求解f(n, other variables)的時候,我們直接默認f(n - 1, other variables)已經(jīng)完了就可以了!這個在前面已經(jīng)解釋過了,在此不再鰲述。
我們將n-1個盤子當作一個整體:這就是類似于分治求解子問題的思想
那么,前一步也就是f(n - 1, other variables)顯然是先將n -1 個在A柱子上的盤子通過C柱移動到B柱上,再將在A柱子上的編號為n的盤子移動到C柱上,再將B柱子上的n-1個盤子通過A柱移動到C柱上
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+7;
const ll mod=1e9+7;
ll cnt,n;
void move(ll id,char from,char to)
{printf ("step %lld: move %lld from %c->%c\n", ++cnt, id, from, to);
}
void hanoi(ll n,char x,char y,char z)
{if(n==0)return;hanoi(n-1,x,z,y);//把n-1個盤子全部從X經(jīng)過z移動到y(tǒng)柱上move(n,x,z);//偷偷把第n個盤子從x移動到z上hanoi(n-1,y,x,z);//把n-1個盤子從y經(jīng)過x移動到z柱上,結束
}int main()
{cin>>n;hanoi(n,'A','B','C');return 0;
}
漢諾塔公式:ans=2n-1
牛牛的漢諾塔
牛牛的漢諾塔
輸入:
3
就是按照經(jīng)典漢諾塔的寫法寫遞歸函數(shù),然后再寫一個結構體,巧妙地用數(shù)組把六種情況存起來即可
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+7;
const ll mod=1e9+7;
ll cnt,n;
struct node
{ll date[6];node(){memset(date,0,sizeof date);}//a->b 0//a->c 1//b->a 2//b->c 3//c->a 4//c->b 5
};
node dp[3][3][3][105];
bool vis[3][3][3][105];
node operator+(const node &a,const node &b)
{node c;for(ll i=0;i<=5;++i){c.date[i]=a.date[i]+b.date[i];}return c;
}
void moveto(ll x,ll y,node &temp)
{if(x==0&&y==1)++temp.date[0];if(x==0&&y==2)++temp.date[1];if(x==1&&y==0)++temp.date[2];if(x==1&&y==2)++temp.date[3];if(x==2&&y==0)++temp.date[4];if(x==2&&y==1)++temp.date[5];return;
}
node hanoi(ll a,ll b,ll c,ll n)//把a經(jīng)過b移動到c柱上(第n個)
{if(vis[a][b][c][n])return dp[a][b][c][n];if(n==1){moveto(a,c,dp[a][b][c][n]);vis[a][b][c][n]=true;//標記一下return dp[a][b][c][n];}node temp;//普通的hanoi塔temp=temp+hanoi(a,c,b,n-1);//先把n-1個從a經(jīng)過c移動到bmoveto(a,c,temp);//把第n個直接從a移動到ctemp=temp+hanoi(b,a,c,n-1);//再把剛剛那n-1個從b經(jīng)過a移動到cvis[a][b][c][n]=true;//完成!return dp[a][b][c][n]=temp;
}
int main()
{cin>>n;node ans=hanoi(0,1,2,n);printf("A->B:%lld\n",ans.date[0]);printf("A->C:%lld\n",ans.date[1]);printf("B->A:%lld\n",ans.date[2]);printf("B->C:%lld\n",ans.date[3]);printf("C->A:%lld\n",ans.date[4]);printf("C->B:%lld\n",ans.date[5]);printf("SUM:%lld\n",(ll(1)<<n)-1);//公式ans=2^n-1
}
注:如果您通過本文,有(qi)用(guai)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區(qū)指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意為您解答!( ?? ω ?? )?
總結
- 上一篇: CoreJava 笔记总结-第四章 对象
- 下一篇: CoreJava 笔记总结-第五章 继承