1. 程式人生 > >南郵演算法分析與設計實驗1 分治策略

南郵演算法分析與設計實驗1 分治策略

分治策略

實驗目的:

理解分治法的演算法思想,閱讀實現書上已有的部分程式程式碼並完善程式,加深對分治法的演算法原理及實現過程的理解。

實驗內容:

用分治法實現一組無序序列的兩路合併排序和快速排序。要求清楚合併排序及快速排序的基本原理,程式設計實現分別用這兩種方法將輸入的一組無序序列排序為有序序列後輸出。

程式碼:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

void Swap(int &a, int &b)
{
	int t = a;
	a = b;
	b = t;
}

template <class T>
class SortableList
{
	public:
		SortableList(int m)
		{
			n = m;
		}
		void MergeSort();
		void Merge(int left, int mid, int right);
		void QuickSort();
		void Input();
		void Init();
		void Output();
	private:
		int RPartition(int left, int right);
		int Partition(int left, int right);
		void MergeSort(int left, int right);
		void QuickSort(int left, int right);
		T l[1000];//輸入的陣列值
		T a[1000];//實際排序物件
		int n;
};


template<class T>
void SortableList<T>::Input()
{
	for(int i = 0; i < n; i++)
		cin >> l[i];
}
//Init()函式的作用是在兩路合併排序結束後將序列恢復到初始序列
//再進行快速排序
template<class T>
void SortableList<T>::Init()
{
	for(int i =0; i < n; i++)
		a[i] = l[i];
}
template<class T>
void SortableList<T>::Output()
{
	for(int i = 0; i < n; i++)
		cout << a[i] << " ";
	cout << endl << endl;
}

//兩路合併排序
template<class T>
void SortableList<T>::MergeSort()
{
	MergeSort(0, n - 1);
}
template<class T>
void SortableList<T>::MergeSort(int left, int right)
{
	if(left < right)
	{
		int mid = (left + right) / 2;
		MergeSort(left, mid);
		MergeSort(mid + 1, right);
		Merge(left, mid, right);
	}
}
template <class T>
void SortableList<T>::Merge(int left, int mid, int right)
{
	T* temp =new T[right - left + 1];
	int i = left, j = mid + 1, k = 0;
	while((i <= mid)&&(j <= right))
		if(a[i] <= a[j])
			temp[k ++] = a[i ++];
		else
			temp[k ++] = a[j ++];
    while(i <= mid)
        temp[k ++] = a[i ++];
    while(j <= right)
        temp[k ++] = a[j ++];
    for(i = 0, k = left; k <= right;)
        a[k ++] = temp[i ++];
}

//快速排序
template <class T>
int SortableList<T>::RPartition(int left, int right)
{
	srand((unsigned)time(NULL));
	int i = rand() % (right - left) + left;
	Swap(a[i], a[left]);
	return Partition(left, right);
}
template <class T>
int SortableList<T>::Partition(int left, int right)
{
	int i = left, j = right + 1;
	do
	{
		do i ++; while(a[i] < a[left]);
		do j --;	while(a[j] > a[left]);
		if(i < j)
			Swap(a[i], a[j]);
	}while(i < j);
	Swap(a[left], a[j]);
	return j;
}
template <class T>
void SortableList<T>::QuickSort(int left, int right)
{
	if(left < right)
	{
		int j = RPartition(left, right);
		QuickSort(left, j - 1);
		QuickSort(j + 1, right);
	}
}
template<class T>
void SortableList<T>::QuickSort()
{
	QuickSort(0, n - 1);
}

int main()
{
	int m;
	cout << "陣列長度n: ";
	cin >> m;
	SortableList<int> List(m);
	cout << "輸入" << m << "個數字:" << endl;
	List.Input();
	List.Init();//得到初始狀態
	List.MergeSort();
	cout << "兩路合併排序後:" << endl;
	List.Output();
	List.Init();//恢復初始狀態
	cout << "快速排序後:" << endl;
	List.QuickSort();
	List.Output();
	return 0;

}

兩者比較:

問題分解過程:

合併排序——將序列一分為二即可。 (十分簡單)

快速排序——需呼叫 Partition 函式將一個序列劃分為子序列。(分解方法相對較困難)

子 問 題 解合併得到原 問題解的過程:

合併排序——需要呼叫 Merge 函式來實現。(Merge 函式時間複雜度O(n))
快速排序——一旦左、右兩個子序列都已分別排序,整個序列便自然成為有序序列。(異 常簡單,幾乎無須額外的工作,省去了從子問題解合併得到原問題解的過程)

思考題:

1、在上述快速排序演算法的執行過程中,跟蹤程式的執行會發現,若初始輸入序列遞減有序, 則呼叫 Partition 函式進行分劃操作時,下標 i 向右尋找大於等於基準元素的過程中會產生 下標越界,為什麼?如何修改程式,可以避免這種情況的發生?

答:這是因為原有的程式在序列最右邊沒有設一個極大值作為哨兵,則下標 i 在向右尋找大 於等於基準元素的過程中,一直沒有滿足條件的元素值出現,就一直不會停止,直至越界。所以只要在序列的最後預留一個哨兵元素,將它的值設為一個極大值比如INT_MAX就可以解決

2、分析這兩種排序演算法在最好、最壞和平均情況下的時間複雜度。

兩路合併排序:最好、最壞、平均情況下的時間複雜度均為 O(nlogn)。

快速排序:最好、平均情況下的時間複雜度為 O(nlogn),最壞情況下為 O(n2)。

3、當初始序列是遞增或遞減次序排列時,通過改進主元(基準元素)的選擇方法,可以提 高快速排序演算法執行的效率,避免最壞情況的發生。

有三種主元選擇方式。 一是取 K(left+right)/2 為主元;

二是取 left~right 之間的隨機整數 j,以 Kj 作為主元;

三是取 Kleft、K(left+right)/2 和 Kright 三者的中間值為主元。

實驗程式採取的是第二種方法


分治法例項  線段樹

   談到分治法首先想到的肯定是二分搜尋,以及快速排序,兩路合併排序,這些都是經典的採用分治策略解決問題的演算法,我要介紹的是一種非常方便的採用了分治法思想的資料結構——線段樹

   線段樹實質上是一棵二叉搜尋樹,它非常方便的解決了很多區間操作的問題,線段樹既是線段也是樹,每個結點是一條線段,每條線段的左右兒子線段分別是該線段的左半和右半區間,遞迴定義之後就是一棵線段樹,首先我們先要知道這樣的資料結構用什麼來儲存,通常使用一個結構體陣列:

struct Tree            //樹結構體
{
    int left, right;    //樹的左右子樹
}tree[MAX];
//MAX通常設為節點數的4倍

接下來看一下基礎線段樹中的幾個函式:

 1.Build函式,顧名思義就是建樹函式,通過這個函式我們可以建立一棵線段樹。

void Build(int l, int r, int step)  //建樹,step代表樹節點的編號(以下均是)
{
    tree[step].left = l;    //賦值
    tree[step].right = r;
    if(l ==r)    //l == r時說明伸展到葉子節點,返回
        return;
    int mid = (l+ r) >> 1;   //二分建樹
    Build(l,mid, step<<1);        //遞迴左子樹
    Build(mid+1,r, (step<<1)+1);  //遞迴右子樹
}

2. Update函式,用來更新線段樹

void Update(int l, int r, int value, int step) 
{
    if(tree[step].left== tree[step].right)   //一直更新到葉子節點返回
        return;
    int mid = (tree[step].left +tree[step].right) >> 1;
    //如果所要更新的點的右端點小於mid或左端點大於mid,則直接更新l到r的值
    if(r <= mid)  
        Update(l,r, value, step<<1);
    else if(l> mid)
        Update(l,r, value, (step<<1)+1);
    //如果所要更新的點在mid的兩邊,則兩邊分別更新
    else 
    {   
        Update(l, mid, value, step<<1);
        Update(mid+1, r, value, (step<<1)+1);
    }
}

3. Query函式,用來查詢區間值

int Query(int l, int r, int step) 
{
    //找到葉子返回葉子值
    if(l == tree[step].left && r ==tree[step].right)
        return tree[step].值;
    int mid = (tree[step].left +tree[step].right) >> 1;
    //查詢和更新類似,都是分段操作
    if(r <=mid)
        return Query(l, r, step<<1);
    if(l >mid)
        return Query(l, r, (step<<1)+1);
    else
        return Query(l, mid, step<<1)+Query(mid+1, r, (step<<1)+1);
}

以上三個函式構成了線段樹的基本結構,我們可以很清楚的看出它的分治思想,線段樹有很多種,比如值的單點更新和值的區間更新等,我們這裡就通過一個例項簡單介紹線段樹裡最簡單的一種應用及線段樹單點更新,顧名思義,每次只更新一個節點的值,下面來看這個問題,來自HDUOJ 1166

敵兵佈陣

Problem Description

C國的死對頭A國這段時間正在進行軍事演習,所以C國間諜頭子Derek和他手下Tidy又開始忙乎了。A國在海岸線沿直線佈置了N個工兵營地,Derek和Tidy的任務就是要監視這些 工兵營地的活動情況。由於採取了某種先進的監測手段,所以每個工兵營地的人數C國都掌握的一清二楚,每個工兵營地的人數都有可能發生變動,可能增加或減少 若干人手,但這些都逃不過C國的監視。
中央情報局要研究敵人究竟演習什麼戰術,所以Tidy要隨時向Derek彙報某一段連續的工兵營地一共有多少人,例如Derek問:“Tidy,馬上彙報 第3個營地到第10個營地共有多少人!”Tidy就要馬上開始計算這一段的總人數並彙報。但敵兵營地的人數經常變動,而Derek每次詢問的段都不一樣, 所以Tidy不得不每次都一個一個營地的去數,很快就精疲力盡了,Derek對Tidy的計算速度越來越不滿:"你個死肥仔,算得這麼慢,我炒你魷 魚!”Tidy想:“你自己來算算看,這可真是一項累人的工作!我恨不得你炒我魷魚呢!”無奈之下,Tidy只好打電話向計算機專家 Windbreaker求救,Windbreaker說:“死肥仔,叫你平時做多點acm題和看多點演算法書,現在嚐到苦果了吧!”Tidy說:"我知錯 了。。。"但Windbreaker已經掛掉電話了。Tidy很苦惱,這麼算他真的會崩潰的,聰明的讀者,你能寫個程式幫他完成這項工作嗎?不過如果你的 程式效率不夠高的話,Tidy還是會受到Derek的責罵的.

Input

第一行一個整數T,表示有T組資料。
每組資料第一行一個正整數N(N<=50000),表示敵人有N個工兵營地,接下來有N個正整數,第i個正整數ai代表第i個工兵營地裡開始時有ai個人(1<=ai<=50)。
接下來每行有一條命令,命令有4種形式:
(1) Add i j,i和j為正整數,表示第i個營地增加j個人(j不超過30)
(2)Sub i j ,i和j為正整數,表示第i個營地減少j個人(j不超過30);
(3)Query i j ,i和j為正整數,i<=j,表示詢問第i到第j個營地的總人數;
(4)End 表示結束,這條命令在每組資料最後出現;
每組資料最多有40000條命令

 Output

對第i組資料,首先輸出“Case i:”和回車,
對於每個Query詢問,輸出一個整數並回車,表示詢問的段中的總人數,這個數保持在int以內。

 Sample Input

1

10

1 2 3 4 5 6 7 8 9 10

Query 1 3

Add 3 6

Query 2 7

Sub 10 2

Add 6 3

Query 3 10

End

Sample Output

Case 1:

6

33

59

題目分析:題意很簡單,就是給一個區間有三個操作,Add表示加人,Sub表示減人,Query表示查詢區間人數和,對於這種區間的更新查詢類問題,一下就能想到上面介紹的線段樹,基本操作部分就不寫註釋了,上面已經介紹過,下面看AC程式碼

#include <cstdio>
#include <cstring>
int const MAX = 400000 + 10;
struct Tree            //樹結構體
{
    int left, right;     //樹的左右子樹
    int sum;            //樹的值
}tree[MAX];
void Build(int l, int r, int step)  //建樹,step代表樹節點的編號(以下均是)
{
    tree[step].left = l;    
    tree[step].right = r;
    tree[step].sum = 0;
    if(l == r)    //l == r時說明伸展到葉子節點,返回
        return;
    int mid = (l + r) >> 1;   //二分建樹
    Build(l, mid, step<<1);
    Build(mid+1, r, (step<<1)+1);
}

void Update(int l, int r, int value, int step)  //更新函式
{
    tree[step].sum += value;   //因為這裡是求和,所以直接將遍歷到的點數值加上所傳值
    if(tree[step].left == tree[step].right)   //一直更新到葉子節點返回
        return;
    int mid = (tree[step].left + tree[step].right) >> 1;
    if(r <= mid)   //如果所要更新的點的右端點小於mid,則直接更新l到r的值
        Update(l, r, value, step<<1);
    else if(l > mid)  //如果所要更新的點的左端點大於mid,則直接更新l到r的值
    //注意上面不能寫成 r<mid 和 l>=mid 由樹的性質決定,讀者可以畫圖看出
        Update(l, r, value, (step<<1)+1);
    else  //如果所要更新的點在mid的兩邊,則兩邊分別更新
    {
        Update(l, mid, value, step<<1);
        Update(mid+1, r, value, (step<<1)+1);
    }
}

int Query(int l, int r, int step)  //查詢函式
{
    if(l == tree[step].left && r == tree[step].right) //找到葉子返回葉子值
        return tree[step].sum;
    int mid = (tree[step].left + tree[step].right) >> 1;//以下類似更新步驟,不再闡述
    if(r <= mid)
        return Query(l, r, step<<1);
    if(l > mid)
        return Query(l, r, (step<<1)+1);
    else
        return Query(l, mid, step<<1) + Query(mid+1, r, (step<<1)+1);
}

int main()
{
    int T;
    int a, b, n;
    char cmd[6];
    scanf("%d",&T);
    for(int i = 1; i <= T; i++)
    {
        scanf("%d",&n); 
        Build(1,n,1);  //1-n建樹
        for(int j = 1; j <= n; j++)
        {
            int temp;
            scanf("%d",&temp);
            Update(j,j,temp,1);   //從根一直更新到葉子
        }
        printf("Case %d:\n",i);
        while(scanf("%s", cmd) != EOF && strcmp(cmd,"End") != 0)
        {
            scanf("%d %d",&a, &b);
            if(strcmp(cmd,"Query") == 0)
                printf("%d\n", Query(a,b,1));
            else if(strcmp(cmd,"Add") == 0)  //加的時候b的值為正
                Update(a,a,b,1);
            else if(strcmp(cmd,"Sub") == 0)  //減的時候b的值為負
                Update(a,a,-b,1);
        }
    }
}

通過對線段樹的概念以及對單點更新例項的介紹,我們可以很清晰的感受到它將分治思想運用的淋漓盡致,將問題通過二叉樹的形式直接分成若干子區間,每個區間完成相應的更新,最終彙總成問題的答案

相關推薦

演算法分析設計實驗1 分治策略

分治策略 實驗目的: 理解分治法的演算法思想,閱讀實現書上已有的部分程式程式碼並完善程式,加深對分治法的演算法原理及實現過程的理解。 實驗內容: 用分治法實現一組無序序列的兩路合併排序和快速排序。要求清楚合併排序及快速排序的基本原理,程式設計實現分別用這兩種方法將輸入的

演算法分析設計實驗 分治策略 兩路合併排序和快速排序

實驗目的理解分治法的演算法思想,閱讀實現書上已有的部分程式程式碼並完善程式, 加深對分治法 的演算法原理及實現過程的理解。實驗內容用分治法實現一組無序序列的兩路合併排序和快速排序。要求清楚合併排序及快速排序 的基本原理, 程式設計實現分別用這兩種方法將輸入的一組無序序列排序

演算法分析設計C++ 1:猴子吃桃

總時間限制: 1000ms 單個測試點時間限制: 100ms 記憶體限制: 655

演算法分析設計基礎(1)漢諾塔問題

問題描述就不說了,自行百度。問題求解的思路本來想用文字描述一下的,結果發現知乎上有人發了個圖,我覺得解釋的十分清楚。下面貼圖: 總結出來一共就三步: 將底盤n以上的環(n-1個)移動到B將

演算法分析設計課程(1):Add Two Numbers

Description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and

130242014019-(2)-“電商系統某功能模塊”需求分析設計實驗課小結

img 商品 歷史記錄 模型 需求分析 今天 ges 關鍵字搜索 識別 1)選題討論 今天主要討論的是電商系統中某一個功能模塊的分析,一個電商系統中有很多個功能模塊,如搜索、登錄、購物車等等。我們組選取了其中的最經常使用的搜索功能進行討論。 2)用戶故事討論 1.用戶可

130242014030(2)“電商系統某功能模塊”需求分析設計實驗課小結

img .com http 二級 電商系統 src 意義 感覺 用戶   這次課老師為了讓我們更加理解敏捷開發,特意請來了王經理給我們介紹。王經理通過讓我們分組,以小組的方式來體驗一下敏捷開發。   分組才用了報數,數字相同的為一組。小組裏沒有明確的分工,大家一起討論,再由

130242014067(2)“電商系統購物車功能模塊”需求分析設計實驗課小結

京東 blog 每次 有一個 並且 小結 應該 快速 後臺 1)分組情況介紹,小組分工合作情況介紹。 陳鋒、劉鑫(用戶故事的細化,即功能設計) 高忠傑、羅成龍(參與系統的類圖設計及上臺匯報) 顏貴榮、李清燦(參與用戶故事的討論與設計) 王紹華、丁天奇、林偉領(參與系統的類圖

130242014053 (2) “電商系統某功能模塊”需求分析設計實驗課小結

記錄 關鍵字 軟件 cmm 思想 管理 電商系統 交流 史記 電商系統的搜索功能模塊 一、分組情況 組長:蔡誌峰 組員:樊鎮霄、林夢遠、曾子雲、謝添華,吳幫莉、周陳清、陳敬龍 二、選題討論 經過投票選擇,我們小組決定以電商系統的搜索功能模塊作為我們的選題。

演算法分析設計課程設計-Dijkstra最短路徑演算法

演算法分析與設計課程設計報告書         題目:Dijkstra最短路徑演算法 設計人:張欽穎 班級:14計科2班    學號:1414080901218   一、  

演算法分析設計期中測試——拓撲序[Special judge]

在圖論中,拓撲序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列. 且該序列必須滿足下面兩個條件: 每個頂點出現且只出現一次. 若存在一條從頂點 A 到頂點 B 的路徑,那麼在序列中頂點

演算法分析設計期中測試——最小和

從數列A[0], A[1], A[2], …, A[N-1]中選若干個數,要求對於每個i(0<=i< N-1),A[i]和A[i+1]至少選一個數,求能選出的最小和. 1 <= N <= 100000, 1 <= A[i] <= 1000 請為下面

演算法分析設計之多處最優服務次序問題

#include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std; int main() { int i,n,j,k

演算法分析設計之多處最優服務次序問題2

¢ 設有n個顧客同時等待一項服務,顧客i需要的服務時間為ti,1≤i≤n,共有s處可以提供此項服務。應如何安排n個顧客的服務次序才能使平均等待時間達到最小?平均等待時間是n個顧客等待服務時間的總和除以n。 ¢ 給定的n個顧客需要的服務時間和s的值,程式設計計算最優服務次序。 ¢ 輸入 第一行

演算法分析設計:動態規劃之矩陣鏈乘

矩陣鏈乘問題 對於給定的n個矩陣,M1, M2 ,…, Mn,其中矩陣Mi 和Mj 是可乘的,要求確定計算矩陣連乘積 ( M1M2 …Mn )的計算次序,使得按照該次數計算 矩陣連乘積時需要的乘法次數最少 1、描述最優解結構 目標: 求出矩陣鏈乘Mi Mi+1 ┅Mj-1 Mj(

C/C++ 演算法分析設計:貪心(整數配對)

題目描述 江鳥想到一個有趣的問題:給你N個正整數,你可以將這N個整數按兩個一組的方式成對劃分,當然其中的元素也可以不和其他元素配對劃分。現在的問題是,讓劃分為一對的元素的乘積與未配對的元素相加求和,並且讓和最大。比如:考慮這個集合{0,1,2,4,5,3},如果我們讓{0,3}、{2,5}分別成

C/C++ 演算法分析設計:貪心(排隊接水)

題目描述 N個人同時提水到一個水龍頭前提水因為大家的水桶大小不一,所以水龍頭注滿第i(i=1,2,3......N)個人所需要的時間是T(i) 編寫一個程式,對這N個人使他們花費的時間總和最小,並求出這個時間。 例如有三個人a,b,c,用時分別是2,1,3 排隊順序為c,b,a的時候,c要等

C/C++ 演算法分析設計:貪心(守望者的逃離)

題目描述 惡魔獵手尤迫安野心勃勃.他背叛了暗夜精靈,率深藏在海底的那加企圖叛變:守望者在與尤迪安的交鋒中遭遇了圍殺.被困在一個荒蕪的大島上。為了殺死守望者,尤迪安開始對這個荒島施咒,這座島很快就會沉下去,到那時,刀上的所有人都會遇難:守望者的跑步速度,為17m/s, 以這樣的速度是無法逃離荒島的

演算法分析設計第十四次作業(leetcode中Cherry Pickup題解)

題解正文 題目描述 問題分析 此題給出一個n乘n矩陣,矩陣中值可以是0/1/-1。 要求我們找出從(0,0)出發,到(n-1,n-1),然後回到(0,0)的路徑,要求往程只能向右向下,而返程只能向左向上走,並且路徑沒有經過值為-1的位置。 然後求出符合上述要求的路徑中,所經

演算法分析設計(一)

一、演算法的定義 滿足五個條件:可行性、確定性、輸入、輸出、有窮性 滿足前四個條件為計算過程(OS) 二、演算法複雜性分析 時間複雜性:對該輸入需要產生的原子操作的步數(是輸入大小的函式) 空間複雜性:演算法所需要的儲存空間 三、計算複雜性函式的階 階:描述增長