ch2 遞歸與分治策略
這部分考覈內容:
二分搜索技術
代碼
/**
* 二分搜索核心代碼
*
* @param a: 查詢數組
* @param x: 查詢的值
* @param n: 數組a的個數
* @return 查找到的下標值,若沒有找到返回-1
*/
int BinarySearch(Type a[], const Type& x, int n)
{
int left = 0;
int right = n-1;
while(left <= right)
{
int middle = (left+right)/2;
if(x == a[middle])
return middle;
else if(x > a[middle])
left = middle + 1;
else
right = middle - 1;
}
return -1;
}
合併排序
例子
序列爲{8, 5, 2, 4, 9}
代碼
/**
* 合併排序算法
*
* @param a: 查詢數組
* @param left: 左指針下標
* @param right: 右指針下標
* @return
*/
void MergeSort(Type a[], int left, int right)
{
if(left < right)
{
int i = (left+right)/2; //取中點
MergeSort(a, left, i);
MergeSort(a, i+1, right);
Merge(a, b, left, i, right);//合併到數組b
Copy(a, b, left, right); //複製回數組a
}
}
/**
* 合併數組:將c[1:m]和c[m+1:r]合併到d[1:r]
*
* @param c 待合併數組
* @param d 合併完成的數組
* @param l 左指針下標
* @param m 中間指針下標
* @param r 右指針下標
* @return
*/
void Merge(Type c[], Type d[], int l, int m, int r)
{
int i = l;
int j = m+1;
int k = l;
while((i <= m) && (j <= r))
{
if(c[i] <= c[j])
d[k++] = c[i++];
else
d[k++] = c[j++];
}
if(i>m) //前半段全都取走了,後半段剩餘直接搬走
{
for(int q=j; q<=r; q++)
d[k++] = c[q];
}
else
{
for(int q=i; q<=m; q++)
d[k++] = c[q];
}
}
代碼陳述
取中間元素分爲左右兩段,分別對左右兩段排序,合併兩段有序的序列,對左右兩段的排序是遞歸使用這個方法
大事化小,如何體現
開始對於一個大數組進行排序,將其切分成兩個小數組,對這兩個小數組進行排序
快速排序
例子
代碼
/**
* 快排核心代碼
*
* @param a: 待排序數組
* @param p: 左邊界
* @param r: 右邊界
* @return
*/
void QuickSort(Type a[], int p, int r)
{
if(p < r)
{
int q = Partition(a, p, r);
QuickSort(a, p, q-1); //對左半段排序
QuickSort(a, q+1, r); //對右半段排序
}
}
/**
* 找標杆點位置
*
* @param a: 目標數組
* @param p: 左邊界
* @param r: 右邊界
* @return 標杆下標
*/
int Partition(Type a[], int p, int r)
{
int i = p;
int j = r+1;
Type x = a[p];
// 將小於x的元素交換到左邊區域,將大於x的元素交換到右邊區域
while(true)
{
while(a[++i]<x && i<r) ;//找到大於等於標杆元素 退出
while(a[--j]>x) ; //找到小於標杆元素 退出
if(i >= j)
break; //i,j異常,則退出
Swap(a[i], a[j]); //交換
}
//把標杆換到位
a[p] = a[j];
a[j] = x;
return j;
}
代碼陳述
對於輸入的數組,首先選取一個標杆,然後用兩個指針指向剩餘的元素兩段,左指針找到比標杆大的值,右指針找到比標杆小的值,然後交換,左右指針異常時,就確定了標杆的位置在右指針位置。然後對於該標杆左右兩段繼續使用上述方法
大事化小,如何體現
本來是對一個大數組進行排序,一輪下來變成對兩個小數組進行排序即可
ch3 動態規劃
這一部分考覈內容:
矩陣連乘問題
例子
老師所給示例
書上示例
轉移方程
代碼
/**
* 矩陣連乘核心代碼
*
* @param p: 矩陣列數(第一個爲數爲 矩陣行數)
* @param n: 矩陣個數
* @param m: m[i][j]表示第i個矩陣到第j個矩陣,這樣的矩陣串最優方案時,所需的最少數乘次數
* @param s: 斷點位置
* @return
*/
void MatrixChain(int *p, int n, int **m, int **s)
{
for(int i=1; i<=n; i++) //填充對角線
m[i][i] = 0;
for(int r=2; r<=n; r++) //r是段長
{
for(int i=1; i<=n-r+1; i++) //i是段起點
{
int j=i+r-1; //j是段終點
// 第一個矩陣一段,後面矩陣一段,計算數乘次數
m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];
s[i][j] = i;
for(int k=i+1; k<j; k++) //遍歷所有斷裂的情況
{
// 在k處斷裂,計算此時數乘次數
int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if(t < m[i][j]) //更新
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
}
大事化小,如何體現
開始對於1到n個矩陣求最少數乘次數,裂開變成對兩段矩陣求最少數乘次數加上兩段的乘積次數
圖像壓縮
例子
轉移方程
代碼
/**
* 圖像壓縮核心代碼
*
* @param n: 像素個數
* @param p: 每個像素的值
* @param s: s[i]表示前i個像素的最優存儲位數
* @param l: l[i]表示第i段的最優段長
* @param b: 每個像素的長度
* @return
*/
void Compress(int n, int p[], int s[], int l[], int b[])
{
int Lmax = 256, header = 11;
s[0] = 0;
for(int i=1; i<=n; i++)
{
b[i] = length(p[i]);
int bmax = b[i];
s[i] = s[i-1] + bmax; //一個像素爲一段
l[i] = 1;
for(int j=2; j<=i&&j<=Lmax; j++) //j是各種合法段長的長度
{
if(bmax < b[i-j+1])
bmax = b[i-j+1]; //最長的位數
if(s[i] > s[i-j] + j*bmax) //若更優,則更新
{
s[i] = s[i-j] + j*bmax;
l[i] = j;
}
}
s[i] += header;
}
}
大事化小,如何體現
開始是對n個像素進行壓縮的最優方案,轉變爲前面n-k個像素壓縮和最後k個像素單獨壓縮之和的最優方案
0-1揹包問題
例子
轉移方程
代碼
/**
* o-1揹包問題的動態規劃算法
*
* @param v: 各物品的價值
* @param w: 各物品的重量
* @param c: 揹包容量
* @param n: 物品數量
* @param m: m[i][j]表示容量爲j,從物品i~n中選擇時的最優選法
* @return
*/
void Knapsack(Type v, int w, int c, int n, Type **m)
{
int jMax = min(w[n]-1, c);
//填寫最後一行
for(int j=0; j<=jMax; j++)
m[n][j] = 0;
for(int j=w[n]; j<=c; j++)
m[n][j] = v[n];
for(int i=n-1; i>1; i--) //從倒數第2行填到順數第2行
{
jMax = min(w[i]-1, c);
for(int j=0; j<=jMax; j++)
m[i][j] = m[i+1][j]; //裝不下物品i
for(int j=w[i]; j<=c; j++) //能裝入 在裝入和放棄中選優
m[i][j] = max(m[i+1][j], m[i+1][j-w[i]]+v[i]);
}
m[1][c] = m[2][c];
if(c >= w[1])
m[1][c] = max(m[1][c], m[2][c-w[1]]+v[1]); //算第一行
}
大事化小,如何體現
開始是從n個物品中選擇最優的選擇方案,變成從後面n-1個物品中選擇的最優方案與第1個物品裝入與否之間的最優方案比較,依次遞歸
ch4 貪心算法
這一部分似乎沒有考覈內容
單源最短路徑
- Dijkstra算法的基本思想是,設置頂點集合S,並不斷地做貪心選擇來擴充這個集合
- 一個頂點屬於集合S當且僅當從源到該頂點的最短路徑長度已知
- 設u是G的某一個頂點,把從源到u且中間只經過S中頂點的路稱爲從源到u的特殊路徑,並用數組dist記錄當前每個頂點所對應的最短特殊路徑長度
實現代碼
//4.5 單源最短路徑
#include<iostream>
using namespace std;
#define maxint 10000
template<class Type>
/**
* 單源最短路徑
*
*@param n 結點個數
*@param v 源點編號
*@param dist[i] 源結點v到結點i的最短路徑長度
*@param prev[i] 到達結點i最短路徑時,上一個結點編號
*@param c[i][j] 存放有向圖中i到j的邊的權
*@return
*/
void Dijkstra(int n, int v, Type *dist, int *prev, Type (*c)[6])
{
bool s[maxint]; //用於記錄紅點集
/*
初始化
*/
for(int i=1; i<=n; i++)
{
dist[i] = c[v][i];
s[i] = false;
if(dist[i] == maxint)
prev[i] = 0;
else
prev[i] = v;
}
dist[v] = 0;
s[v] = true;
/*
core
*/
for(int i=1; i<n; i++)
{
int temp = maxint;
int u = v;
for(int j=1; j<=n; j++)
if((!s[j]) && (dist[j]<temp)) //選出不在紅點集 且 路徑最短的結點
{
u = j;
temp = dist[j];
}
s[u] = true;
for(int j=1; j<=n; j++) //更新到所有未加入紅點集節點的最短距離
{
if((!s[j]) && (c[u][j] < maxint))
{
Type newdist = dist[u] + c[u][j];
if(newdist < dist[j])
{
dist[j] = newdist;
prev[j] = u;
}
}
}
}
}
int main()
{
int n=5;
int v=1;
int dist[6] = {0};
int prev[6] = {0};
int c[6][6];
c[1][2]=10; c[1][4]=30; c[1][5]=100;
c[2][3]=50;
c[3][5]=10;
c[4][3]=20; c[4][5]=60;
Dijkstra(n, v, dist, prev, c);
for(int i=2; i<=n; i++){
cout<<"結點1到結點"<<i<<"的最短路徑:"<<dist[i]<<"\t";
cout<<"前一個結點是"<<prev[i]<<endl;
}
return 0;
}
ch5 回溯法
這部分考覈內容:
n後問題
解空間
代碼
/**
* 查看第k行皇后是否與上面衝突
*
* @param k 行數
* @return 是否合法
*/
bool Queen::Place(int k)
{
for(int j=1; j<k; j++)
if((abs(k-j) == abs(x[j]-x[k])) || (x[j] == x[k])) //對角線或同一列
return false;
return true;
}
/**
* n後問題核心算法
*
* @param t 當前處理的行數
* @return
*/
void Queen::Backtrack(int t)
{
if(t > n)
sum++;
else
{
for(int i=1; i<=n; i++)
{
x[t] = i;
if(Place(t))
Backtrack(t+1);
}
}
}
0-1揹包問題
解空間
代碼
/**
* 計算上界
*
* @param i 選擇物品從i往後
* @return 返回價值上界
*/
template<class Typew, class Typep>
Typep Knap<Typew, Typep>::Bound(int i)
{
Typew cleft = c-cw; //揹包剩餘空間
Typep b = cp; //當前價值
while(i<=n && w[i]<=cleft) //以物品單位重量價值遞減序裝入物品
{
cleft -= w[i];
b += p[i];
i++;
}
if(i <= n) //分割物品,裝滿揹包
b += p[i]*cleft/w[i];
return b;
}
圖的m着色(染色)問題
解空間
代碼
/**
* 染色是否衝突
*
* @param k 頂點k
* @return 是否
*/
bool Color::Ok(int k)
{
for(int j=1; j<=n; j++)
if((a[k][j]==1) && (x[j]==x[k])) //檢查相鄰和顏色是否相同
return false;
return true;
}
/**
* 染色問題核心代碼
*
* @param t 第t個頂點
* @return
*/
void Color::Backtrack(int t)
{
if(t>n) //完成所有點的染色
{
sum++; //合法的解個數
for(int i=1; i<=n; i++)
cout<< x[i] << ' ';
cout<<endl;
}
else //未完成所有點的染色
{
for(int i=1; i<=m; i++)
{
x[t] = i; //給t號結點染第i種顏色
if(Ok(t)) //判斷是否合法
Backtrack(t+1); //無衝突,則繼續處理第t+1個結點
x[t] = 0; //洗白,恢復現場
}
}
}
旅行售貨員問題
解空間
x數組的變化情況
略微模糊
爲了更加清晰易看,下面圖是上圖的部分切分版本
x數組的變化情況
j=2, j=3
及其子節點回溯情況
j=2, j=4
及其子節點回溯情況
ch6 分支限界法
這部分的考覈內容:
分支限界法要素
- 解空間
- 界(解空間中說事,解空間的點)
- 當葉子結點出現在隊列頭部時,該結點爲最優
單源最短路徑問題
界是什麼量
- 在代碼中是使用
length
(當前路長,或使用cc
當前開銷) - 解空間樹中的結點所對應的當前路長是以該結點爲根的子樹中所有結點對應路長的一個下界
對於給定數據,隊列的變化情況
陳述算法
首先將源節點入隊列,該隊列是按照當前路長最短進行排列的優先隊列,每次都從隊列頭部取出一個節點,將其擴展的子節點入隊,然後再從隊列頭部取出一個節點,重複上述過程,直到終點節點第一次出現在隊頭爲止,此時該終點的路徑爲最優
0-1揹包問題
界是什麼量
- 在代碼中使用的是
uprofit
(結點的價值上界) - 解空間樹中以結點N爲根的子樹樹任一結點的價值都不超過N的
uprofit
,即 根結點的uprofit
比子孫的uprofit
都大
對於給定數據,隊列的變化情況
上圖圈圈中有三個數,最頂上那個數是當前揹包重量,中間的那個數是當前揹包價值,最底下那個數是當前揹包的上界
陳述算法
首先各物品按其單位重量價值從大到小排序
- 按
uprofit
由大到小進入最大堆 - 解空間根結點入隊
- 取隊頭,計算頭結點的各個孩子,孩子入隊
- 對頭若爲葉結點,算法結束,否則重複上一步驟
旅行售貨員問題
界是什麼量
- 在代碼中使用的是
lcost
(最低消費) - 解空間樹中的結點對應的
lcost
是以該結點爲根的子樹中所有結點對應的lcost
的一個下界
對於給定數據,隊列的變化情況
說明:
- 上圖中的
minout
爲每個頂點的最小費用出邊 - 各點的
lcost
計算是當前開銷+離開其餘未到達的點的最小值 - 例如:
c點lcost
=當前開銷(30元)+離開2的錢最少(5元)+離開3的錢最少(5元)+離開4的錢最少(4)= 44
陳述算法
首先先統計出每個頂點的最小費用出邊minout
- 按
lcost
由小到大進入優先隊列 - 解空間根結點入隊
- 取隊頭,計算頭結點的各個孩子的
lcost
,孩子入隊 - 對頭若爲葉結點,算法結束,否則重複上一步驟
電路板排列問題
界是什麼量
- 在代碼中使用的是
cd
(當前密度) - 解空間樹中的結點對應的
cd
是以該結點爲根的子樹中所有結點對應的cd
的一個下界
對於給定數據,隊列的變化情況
說明:解空間樹中第一層的含義是1號槽放第1/2/3號電路板,其餘層同理
陳述算法
- 按
cd
由小到大進入優先隊列 - 解空間根結點入隊
- 取隊頭,計算頭結點的各個孩子的
cd
,孩子入隊 - 結束情況有兩種
- 若當前擴展結點的
cd
bestd
,則優先隊列中其餘結點都不可能導致最優解,算法結束 - 若已排定n-1塊電路板,則算法結束
- 若當前擴展結點的
說明:bestd
表示目前遇到的每塊板子插好時的最優密度
ch7 隨機化算法
這部分的考覈內容:
n後問題(拉斯維加斯算法)
代碼
/**
* 隨機放置n個皇后的拉斯維加斯算法
*
* @return 是否有解
*/
bool Queen::QueensLV(void)
{
RandomNumber rnd; //隨機數產生器
int k = 1; //下一個放置的皇后編號
int count = 1;
while((k<=n) && (count>0)) //上行有解,且未到最後一行
{
count = 0;
for(int i=1; i<=n; i++) //統計並記錄當前本行的所有合法位置
{
x[k] = i; //k行i列
if(Place(k)) //判斷是否位置是否合法
y[count++] = i;
}
if(count>0)
x[k++] = y[rnd.Random(count)]; //從合法位置中隨機選一個
}
return (count>0); //count>0表示所有皇后放置成功
}
素數測試(蒙特卡羅算法)
相關知識
費爾馬小定理:如果p是一個素數,且0<a<p,則ap-1≡1(mod p)
- 利用費爾馬小定理,對於給定的整數n,可以設計素數判定算法
- 通過計算d=2n-1mod n 來判定整數n的素性
- 當d≠1時,n肯定不是素數
- 當d=1時,n 很可能是素數
- 費爾馬小定理畢竟只是素數判定的一個必要條件,滿足費爾馬小定理條件的整數n未必全是素數。
- 有些合數也滿足費爾馬小定理的條件,這些合數被稱爲Carmichael數,前3個Carmichael數是561、1105、1729。Carmichael數是非常少的,在1~100 000 000的整數中,只有255個Carmichael數
二次探測定理:如果p是一個素數,且0<x<p,則方程x2≡1(mod p)的解爲x=1,p-1
- 事實上,x2≡1(mod p)等價於x2-1≡0(mod p)。由此可知(x-1)(x+1)≡0(mod p),故p必須整除x-1或x+1,由於p是素數且0<x<p,推出x=1或x=p-1
- 利用二次探測定理,可以在利用費爾馬小定理計算an-1mod n 的過程中增加對整數n的二次探測。一旦發現違背二次探測條件,即可得出n不是素數的結論
- 下面是算法power用於計算apmod n,並在計算過程中實施對n的二次探測
/** * 費爾馬小定律並實施n的二次探測 * a^p mod n =result * * @param a: 費爾馬小定律中的底數 * @param p: 需要判斷是否爲素數的數字 * @param n: * @param result: 計算結果 * @param composite: * @return */ void power(unsigned int a, unsigned int p, unsigned int n, unsigned int &result, bool &composite) { unsigned int x; if(p==0) result = 1; else { power(a, p/2, n, x, composite); // 遞歸計算 result = (x*x)%n; //二次探測 (A*B)%n = (A%n)*(B%n) if((result==1) && (x!=1) && (x!=n-1)) composite = true; if((p%2)==1) result = (result*a) % n; } }
算法陳述
- 對於數n,在1、2、…、n-1中隨機抽樣a
- 若a不讓an-1≡1(mod n)成立,則n是合數
- 若a使an-1≡1(mod n)成立,則重複抽樣k次,若都成立,則傾向於n是素數,誤判的概率爲(1/4)k
誤判概率
- 當n充分大時,1~n-1的基數中有1/4個數是使費爾馬小定律成立的,但n未必是素數,所以我們隨機選到這樣一個基數的概率爲1/4
- 若進行k次測試,誤判的概率爲(1/4)k