



1.1第1 章─概論
1.1.1練習題
1.下列關於算法的說法中正確的有()。
Ⅰ.求解某一類問題的算法是唯一的
Ⅱ.算法必須在有限步操作之后停止
Ⅲ.算法的每一步操作必須是明確的,不能有歧義或含義模糊
Ⅳ.算法執行后一定產生確定的結果
A. 1 個B.2 個C.3 個D.4 個
2. T(n)表示當輸入規模為n時的算法效率,以下算法效率最優的是()。
A.T(n)= T(n-1)+1,T(1)=1 C.T(n)= T(n/2)+1,T(1)=1
B.T(n)= 2n
D.T(n)=3nlog2n
3.什么是算法?算法有哪些特征?
4.判斷一個大於2的正整數n是否為素數的方法有多種,給出兩種算法,說明其中一種算法更好的理由。
5.證明以下關系成立:
(1)10n-2n=(n)
(2)2=(2)
6.證明O(f(n))+O(g(n))=O(max{f(n),g(n)})。
7.有一個含n(n>2)個整數的數組a,判斷其中是否存在出現次數超過所有元素一半的元素。
8.一個字符串采用string 對象存儲,設計一個算法判斷該字符串是否為回文。
9.有一個整數序列,設計一個算法判斷其中是否存在兩個元素和恰好等於給定的整數k。
10.有兩個整數序列,每個整數序列中所有元素均不相同。設計一個算法求它們的公共元素,要求不使用STL 的集合算法。
11.正整數n(n>1)可以寫成質數的乘積形式,稱為整數的質因數分解。例如,12=2*2*3,18=2*3*3,11=11。設計一個算法求n這樣分解后各個質因數出現的次數,采用vector 向量存放結果。
12.有一個整數序列,所有元素均不相同,設計一個算法求相差最小的元素對的個數。如序列4、1、2、3的相差最小的元素對的個數是3,其元素對是(1,2),(2,3),(3,4)。
13.有一個map<string,int>容器,其中已經存放了較多元素。設計一個算法求出其中重復的value 並且返回重復value 的個數。
14.重新做第10題,采用map 容器存放最終結果。
15.假設有一個含n(n>1)個元素的stack<int>棧容器st,設計一個算法出棧從棧頂到棧底的第k(1≤k≤n)個元素,其他棧元素不變。









算法設計
1.1.2練習題參考答案
1.答:由於算法具有有窮性、確定性和輸出性,因而Ⅱ、Ⅲ、Ⅳ正確,而解決某一類問題的算法不一定是唯一的。答案為C。
2.答:選項A的時間復雜度為O(n)。選項B的時間復雜度為O(n)。選項C的時間復雜度為O(log2n)。選項D 的時間復雜度為O(nlog2n)。答案為C。
3.答:算法是求解問題的一系列計算步驟。算法具有有限性、確定性、可行性、輸入性和輸出性5 個重要特征。
4.答:兩種算法如下:
#include <stdio.h>
#include <math.h>
boolisPrime1(intn)//方法1
{for (int i=2;i<n;i++)
if (n%i==0)
return false;
return true;
}
boolisPrime2(intn)//方法2
{for (int i=2;i<=(int)sqrt(n);i++)
if (n%i==0)
return false;
return true;
}
voidmain()
{int n=5;
printf("%d,%d\n",isPrime1(n),isPrime2(n));
}
方法1 的時間復雜度為O(n),方法2的時間復雜度為n,所以方法2 更好。5.答:(1)當n 足夠大時,(10n-2n)/( n)=10,所以10n-2n=(n)。
(2)2=2*2=(2)。
6.證明:對於任意f1(n)∈O(f(n)),存在正常數c1 和正常數n1,使得對所有n≥n1,有f1(n)≤c1f(n)。
類似地,對於任意g1(n)∈O(g(n)),存在正常數c2 和自然數n2,使得對所有n≥n2,有g1(n)≤c2g(n)。
令c3=max{c1,c2},n3=max{n1,n2},h(n)= max{f(n),g(n)}。
則對所有的n≥n3,有:
f1(n) +g1(n)≤c1f(n) +c2g(n)≤c3f(n)+c3g(n)=c3(f(n)+g(n))
≤c32max{f(n),g(n)}=2c3h(n)=O(max{f(n),g(n)})。
7.解:先將a 中元素遞增排序,再求出現次數最多的次數maxnum,最后判斷是否滿足條件。對應的程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
2







第1章
概論
boolsolve(inta[],intn,int&x)
{sort(a,a+n);
int maxnum=0;
int num=1;
int e=a[0];
for (int i=1;i<n;i++)
{if (a[i]==e)
{num++;
//遞增排序
//出現次數最多的次數
if (num>maxnum)
{maxnum=num;
x=e;
}
}
else
{e=a[i];
num=1;
}
}
if (maxnum>n/2)
return true;
else
return false;
}
voidmain()
{int a[]={2,2,2,4,5,6,2};
int n=sizeof(a)/sizeof(a[0]);
int x;
if (solve(a,n,x))
printf("出現次數超過所有元素一半的元素為%d\n",x); else
printf("不存在出現次數超過所有元素一半的元素\n");
}
上述程序的執行結果如圖1.1 所示。
圖1.1
程序執行結果
8.解:采用前后字符判斷方法,對應的程序如下:#include <iostream>
#include <string>
using namespace std;
boolsolve(stringstr)//判斷字符串str是否為回文{int i=0,j=str.length()-1;
while (i<j)
{if (str[i]!=str[j])
return false;
3




算法設計
i++; j--;
}
return true;
}
voidmain()
{cout << "求解結果"<<endl;
string str="abcd";
cout << "" << str<<(solve(str)?"是回文":"不是回文")<< endl; string str1="abba";
cout << "" << str1<<(solve(str1)?"是回文":"不是回文")<<endl;}
上述程序的執行結果如圖1.2所示。
圖1.2
程序執行結果
9.解:先將a 中元素遞增排序,然后從兩端開始進行判斷。對應的程序如下:#include <stdio.h>
#include <algorithm>
using namespace std;
boolsolve(inta[],intn,intk)
{sort(a,a+n);
int i=0, j=n-1;
while (i<j)
{if (a[i]+a[j]==k)
return true;
//遞增排序
//區間中存在兩個或者以上元素
else if (a[i]+a[j]<k)
i++;
else
j--;
}
return false;
}
voidmain()
{int a[]={1,2,4,5,3};
int n=sizeof(a)/sizeof(a[0]);
printf("求解結果\n");
int k=9,i,j;
if (solve(a,n,k,i,j))
printf("存在: %d+%d=%d\n",a[i],a[j],k);else
printf("不存在兩個元素和為%d\n",k);
int k1=10;
if (solve(a,n,k1,i,j))
printf("存在: %d+%d=%d\n",a[i],a[j],k1);
4







第1章
概論
else
printf("
不存在兩個元素和為%d\n",k1);
}
上述程序的執行結果如圖1.3 所示。
圖1.3
程序執行結果
10.解:采用集合set<int>存儲整數序列,集合中元素默認是遞增排序的,再采用二路歸並算法求它們的交集。對應的程序如下:
#include <stdio.h>
#include <set>
using namespace std;
voidsolve(set<int>s1,set<int>s2,set<int>&s3){set<int>::iteratorit1,it2;
it1=s1.begin(); it2=s2.begin();
while (it1!=s1.end()&& it2!=s2.end())
{if (*it1==*it2)
{s3.insert(*it1);
++it1; ++it2;
}
else if (*it1<*it2)
++it1;
else
++it2;
}
}
//求交集s3
voiddispset(set<int>s)
{set<int>::iteratorit;
//輸出集合的元素
for (it=s.begin();it!=s.end();++it)
printf("%d ",*it);
printf("\n");
}
voidmain()
{int a[]={3,2,4,8};
int n=sizeof(a)/sizeof(a[0]);
set<int> s1(a,a+n);
int b[]={1,2,4,5,3};
int m=sizeof(b)/sizeof(b[0]);
set<int> s2(b,b+m);
set<int> s3;
solve(s1,s2,s3);
printf("求解結果\n");
printf("s1: ");dispset(s1);
5




算法設計
printf("
printf("
s2: "); dispset(s2);
s3: "); dispset(s3);
}
上述程序的執行結果如圖1.4 所示。
圖1.4
程序執行結果
11.解:對於正整數n,從i=2 開始查找其質因數,ic 記錄質因數i 出現的次數,當找到這樣質因數后,將(i,ic)作為一個元素插入到vector 容器v 中。最后輸出v。對應的算法如下:
#include <stdio.h>
#include <vector>
using namespace std;
struct NodeType
{int p;
int pc;
//vector向量元素類型 //質因數
//質因數出現次數
};
voidsolve(intn,vector<NodeType>&v)//求n的質因數分解{int i=2;
int ic=0;
NodeType e;
do
{if (n%i==0)
{ic++;
n=n/i;
}
else
{if (ic>0)
{e.p=i;
e.pc=ic;
v.push_back(e);
}
ic=0;
i++;
}
} while (n>1 || ic!=0);
}
voiddisp(vector<NodeType>&v)//輸出v
{vector<NodeType>::iteratorit;
for (it=v.begin();it!=v.end();++it)
printf("質因數%d出現%d次\n",it->p,it->pc);
}
6








第1章
概論
voidmain()
{vector<NodeType>v;
int n=100;
printf("n=%d\n",n);
solve(n,v);
disp(v);
}
上述程序的執行結果如圖1.5 所示。
圖1.5
程序執行結果
12.解:先遞增排序,再求相鄰元素差,比較求最小元素差,累計最小元素差的個數。對應的程序如下:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
intsolve(vector<int>&myv)
//求myv中相差最小的元素對的個數
{sort(myv.begin(),myv.end());//遞增排序
int ans=1;
int mindif=myv[1]-myv[0];
for (int i=2;i<myv.size();i++)
{if (myv[i]-myv[i-1]<mindif)
{ans=1;
mindif=myv[i]-myv[i-1];
}
else if (myv[i]-myv[i-1]==mindif)
ans++;
}
return ans;
}
voidmain()
{int a[]={4,1,2,3};
int n=sizeof(a)/sizeof(a[0]);
vector<int> myv(a,a+n);
cout << "相差最小的元素對的個數:" << solve(myv) <<endl; }
上述程序的執行結果如圖1.6 所示。
7




圖1.6
算法設計
程序執行結果
13.解:對於map<string,int>容器mymap,設計另外一個map<int,int>容器tmap,將前者的value 作為后者的關鍵字。遍歷mymap,累計tmap 中相同關鍵字的次數。一個參考程序及其輸出結果如下:
#include <iostream>
#include <map>
#include <string>
using namespace std;
voidmain()
{map<string,int>mymap;
mymap.insert(pair<string,int>("Mary",80));
mymap.insert(pair<string,int>("Smith",82));
mymap.insert(pair<string,int>("John",80));
mymap.insert(pair<string,int>("Lippman",95));
mymap.insert(pair<string,int>("Detial",82));
map<string,int>::iteratorit;
map<int,int> tmap;
for (it=mymap.begin();it!=mymap.end();it++)
tmap[(*it).second]++;
map<int,int>::iteratorit1;
cout << "求解結果"<<endl;
for (it1=tmap.begin();it1!=tmap.end();it1++)
cout << "" << (*it1).first<< ":" << (*it1).second<< "次\n";
}
上述程序的執行結果如圖1.7 所示。
圖1.7
程序執行結果
14.解:采用map<int,int>容器mymap 存放求解結果,第一個分量存放質因數,第二個分量存放質因數出現次數。對應的程序如下:
#include <stdio.h>
#include <map>
using namespace std;
voidsolve(intn,map<int,int>&mymap)//求n的質因數分解
{int i=2;
int ic=0;
do
{
if (n%i==0)
{ic++;
n=n/i;
}
8







第1章
概論
else
{if (ic>0)
mymap[i]=ic;
ic=0;
i++;
}
} while (n>1 || ic!=0);
}
voiddisp(map<int,int>&mymap)//輸出mymap
{map<int,int>::iteratorit;
for (it=mymap.begin();it!=mymap.end();++it)
printf("質因數%d出現%d次\n",it->first,it->second);
}
voidmain()
{map<int,int>mymap;
int n=12345;
printf("n=%d\n",n);
solve(n,mymap);
disp(mymap);
}
上述程序的執行結果如圖1.8 所示。
圖1.8
程序執行結果
15.解:棧容器不能順序遍歷,為此創建一個臨時tmpst 棧,將st 的k個元素出棧並進棧到tmpst 中,再出棧tmpst一次得到第k 個元素,最后將棧tmpst 的所有元素出棧並進棧到st 中。對應的程序如下:
#include <stdio.h>
#include <stack>
using namespace std;
intsolve(stack<int>&st,intk)
{stack<int> tmpst;
int e;
for (int i=0;i<k;i++)
{e=st.top();
st.pop();
tmpst.push(e);
}
e=tmpst.top();
tmpst.pop();
while (!tmpst.empty())
{st.push(tmpst.top());
tmpst.pop();
//出棧第k個元素
//出棧st的k個元素並進tmpst棧//求第k個元素
//將tmpst的所有元素出棧並進棧st9




}
return e;
}
voiddisp(stack<int>&st)
{while (!st.empty())
{printf("%d ",st.top());
st.pop();
}
printf("\n");
}
voidmain()
{stack<int> st;
算法設計
//出棧st的所有元素
printf("進棧元素1,2,3,4\n");st.push(1);
st.push(2);
st.push(3);
st.push(4);
int k=3;
int e=solve(st,k);
printf("出棧第%d個元素是: %d\n",k,e);printf("st中元素出棧順序: "); disp(st);
}
上述程序的執行結果如圖1.9 所示。
圖1.9
程序執行結果
1.2第2 章─遞歸算法設計技術
1.2.1練習題
1.什么是直接遞歸和間接遞歸?消除遞歸一般要用到什么數據結構?2.分析以下程序的執行結果:
#include <stdio.h>
voidf(intn,int&m)
{if (n<1) return;
else
{
}
printf("調用f(%d,%d)前,n=%d,m=%d\n",n-1,m-1,n,m);n--; m--;
f(n-1,m);
printf("調用f(%d,%d)后:n=%d,m=%d\n",n-1,m-1,n,m);
10







第1章
概論
}
voidmain()
{int n=4,m=4;
f(n,m);
}
3.采用直接推導方法求解以下遞歸方程:T(1)=1
T(n)=T(n-1)+n當n>1
4.采用特征方程方法求解以下遞歸方程:H(0)=0
H(1)=1
H(2)=2
H(n)=H(n-1)+9H(n-2)-9H(n-3)當n>2 5.采用遞歸樹方法求解以下遞歸方程:T(1)=1
T(n)=4T(n/2)+n當n>1
6.采用主方法求解以下題的遞歸方程。
T(n)=1
當n=1
T(n)=4T(n/2)+n當n>1
7.分析求斐波那契f(n)的時間復雜度。
8.數列的首項a1=0,后續奇數項和偶數項的計算公式分別為a2n=a2n-1+2,a2n+1=a2n- 1+a2n-1,寫出計算數列第n項的遞歸算法。
9.對於一個采用字符數組存放的字符串str,設計一個遞歸算法求其字符個數(長度)。
10.對於一個采用字符數組存放的字符串str,設計一個遞歸算法判斷str 是否為回文。
11.對於不帶頭結點的單鏈表L,設計一個遞歸算法正序輸出所有結點值。
12.對於不帶頭結點的單鏈表L,設計一個遞歸算法逆序輸出所有結點值。
13.對於不帶頭結點的非空單鏈表L,設計一個遞歸算法返回最大值結點的地址(假設這樣的結點唯一)。
14.對於不帶頭結點的單鏈表L,設計一個遞歸算法返回第一個值為x 的結點的地址,沒有這樣的結點時返回NULL。
15.對於不帶頭結點的單鏈表L,設計一個遞歸算法刪除第一個值為x 的結點。
16.假設二叉樹采用二叉鏈存儲結構存放,結點值為int 類型,設計一個遞歸算法求二叉樹bt 中所有葉子結點值之和。
17.假設二叉樹采用二叉鏈存儲結構存放,結點值為int 類型,設計一個遞歸算法求二叉樹bt 中所有結點值大於等於k 的結點個數。
18.假設二叉樹采用二叉鏈存儲結構存放,所有結點值均不相同,設計一個遞歸算法求值為x 的結點的層次(根結點的層次為1),沒有找到這樣的結點時返回0。
11



























算法設計
1.2.2練習題參考答案
1.答:一個f 函數定義中直接調用f 函數自己,稱為直接遞歸。一個f 函數定義中調用g 函數,而g 函數的定義中調用f 函數,稱為間接遞歸。消除遞歸一般要用棧實現。
2.答:遞歸函數f(n,m)中,n是非引用參數,m是引用參數,所以遞歸函數的狀態為(n)。程序執行結果如下:
調用f(3,3)前,n=4,m=4
調用f(1,2)前,n=2,m=3
調用f(0,1)后,n=1,m=2
調用f(2,1)后,n=3,m=2
3.解:求T(n)的過程如下:
T(n)=T(n-1)+n=[T(n-2)+n-1)]+n=T(n-2)+n+(n-1)
=T(n-3)+n+(n-1)+(n-2)
=…
=T(1)+n+(n-1)+…+2
=n+(n-1)+ +…+2+1=n(n+1)/2=O(n)。
4.解:整數一個常系數的線性齊次遞推式,用x
n-33232
代替H(n),有:x=x+9x-9x,
x-x-9x+9=x(x-9)-(x-9)=(x-1)(x-9)=(x-1)(x+3)(x-3)=0。得到r1=1,r2=-3,r3=3則遞歸方程的通解為:H(n)=c1+c2(-3)+c33
代入H(0)=0,有c1+c2+c3=0
代入H(1)=1,有c1-3c2+3c3=1
代入H(2)=2,有c1+9c2+9c3=2
求出:c1=-1/4,c2=-1/12,c3=1/3,H(n)=c1+c2(-3)+c33=(
n‒1
4
+ 13
1
‒。
5.解:構造的遞歸樹如圖1.10 所示,第1 層的問題規模為n,第2 的層的子問題的問題規模為n/2,依此類推,當展開到第k+1層,其規模為n/2=1,所以遞歸樹的高度為log2n+1。
第1層有1個結點,其時間為n,第2層有4個結點,其時間為4(n/2)=2n,依次類推,第k 層有4個結點,每個子問題規模為n/2,其時間為4(n/2)=2n。葉子結點的個數為n 個,其時間為n。將遞歸樹每一層的時間加起來,可得:
logn
T(n)=n+2n+…+ 2n+…+n≈𝑛∗2=O(n)。
12
































第1章
概論
n
n
(n/2)
(n/2)
(n/2)
(n/2)
2n
高度h 為log2n+1
(n/2)
…
1
(n/2)
…
1
(n/2)
…
1
(n/2)
…
1
2n
n
圖1.10一棵遞歸樹
6.解:采用主方法求解,這里a=4,b=2,f(n)=n。
logalog4
因此,𝑛=𝑛=n,它與f(n)一樣大,滿足主定理中的情況(2),所以T(n)=O( loga
𝑛log2n)=O(nlog2n)。
7.解:設求斐波那契f(n)的時間為T(n),有以下遞推式:
T(1)=T(2)
T(n)=T(n-1)+T(n-2)+1
當n>2
其中,T(n)式中加1表示一次加法運算的時間。
不妨先求T1(1)=T1(2)=1,T1(n)=T1(n-1)+T1(n-2),按《教程》例2.14的方法可以求
出:
T1(n)=
1
5
15
1
5
15
115≈=
所以T(n)=T1(n)+1≈
1
5
15
+1=O(φ),其中φ=。
2
8.解:設f(m)計算數列第m項值。
當m 為偶數時,不妨設m=2n,則2n-1=m-1,所以有f(m)=f(m-1)+2。
當m 為奇數時,不妨設m=2n+1,則2n-1=m-2,2n=m-1,所以有f(m)=f(m-2)+f(m-1)-1。
對應的遞歸算法如下:
intf(int m)
{if (m==1) return0;
if (m%2==0)
return f(m-1)+2;
else
return f(m-2)+f(m-1)-1;
}
9.解:設f(str)返回字符串str的長度,其遞歸模型如下:
f(str)=0
f(str)=f(str+1)+1
當*str='\0'時
其他情況
對應的遞歸程序如下:
13




#include <iostream>
using namespace std;
intLength(char*str)
{if (*str=='\0')
return 0;
else
return Length(str+1)+1;
}
voidmain()
{char str[]="abcd";
算法設計
//求str的字符個數
cout << str << "的長度:"<<Length(str) << endl; }
上述程序的執行結果如圖1.11 所示。
圖1.11
程序執行結果
10.解:設f(str,n)返回含n 個字符的字符串str是否為回文,其遞歸模型如下:
f(str,n)=true
f(str,n)=flase
f(str,n)=f(str+1,n-2)
當n=0 或者n=1 時當str[0]≠str[n-1]時其他情況
對應的遞歸算法如下:
#include <stdio.h>
#include <string.h>
boolisPal(char*str,intn)//str回文判斷算法{if (n==0 ||n==1)
return true;
if (str[0]!=str[n-1])
return false;
return isPal(str+1,n-2);
}
voiddisp(char*str)
{int n=strlen(str);
if (isPal(str,n))
printf("%s是回文\n",str);
else
printf("%s不是回文\n",str);
}
voidmain()
{printf("求解結果\n");
disp("abcba");
disp("a");
disp("abc");
}
14








第1章
概論
上述程序的執行結果如圖1.12 所示。
圖1.12
程序執行結果
11.解:設f(L)正序輸出單鏈表L 的所有結點值,其遞歸模型如下:
f(L)≡ 不做任何事情
f(L)≡ 輸出L->data; f(L->next);對應的遞歸程序如下:#include "LinkList.cpp"
當L=NULL
當L≠NULL 時
//包含單鏈表的基本運算算法
voiddispLink(LinkNode*L)//正序輸出所有結點值{if (L==NULL)return;
else
{printf("%d ",L->data);
dispLink(L->next);
}
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L;
CreateList(L,a,n);printf("正向L: ");dispLink(L); printf("\n");
Release(L);
//由a[0..n-1]創建不帶頭結點的單鏈表 //銷毀單鏈表
}
上述程序的執行結果如圖1.13 所示。
圖1.13
程序執行結果
12.解:設f(L)逆序輸出單鏈表L 的所有結點值,其遞歸模型如下:
f(L)≡ 不做任何事情
f(L)≡ f(L->next);輸出L->data對應的遞歸程序如下:
#include "LinkList.cpp"
voidRevdisp(LinkNode*L)
{if (L==NULL)return;
當L=NULL
當L≠NULL 時
//包含單鏈表的基本運算算法//逆序輸出所有結點值
15




else
{Revdisp(L->next);
printf("%d ",L->data);
}
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L;
CreateList(L,a,n);
printf("反向L: ");
Revdisp(L); printf("\n");
Release(L);
}
上述程序的執行結果如圖1.14 所示。
圖1.14
算法設計
程序執行結果
13.解:設f(L)返回單鏈表L 中值最大結點的地址,其遞歸模型如下:
f(L) = L
f(L) =MAX{f(L->next),L->data}對應的遞歸程序如下:#include "LinkList.cpp"
當L 只有一個結點時其他情況
//包含單鏈表的基本運算算法
LinkNode*Maxnode(LinkNode*L)//返回最大值結點的地址{if (L->next==NULL)
return L;
else
{LinkNode *maxp;
maxp=Maxnode(L->next);
if (L->data>maxp->data)
return L;
else
return maxp;
}
}
voidmain()
{int a[]={1,2,5,2,3,2};
//只有一個結點時
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L,*p;
CreateList(L,a,n);
p=Maxnode(L);
printf("最大結點值:%d\n",p->data); Release(L);
16








第1章
概論
}
上述程序的執行結果如圖1.15 所示。
圖1.15
程序執行結果
14.解:設f(L,x)返回單鏈表L 中第一個值為x 的結點的地址,其遞歸模型如下:
f(L,x) = NULL
f(L,x) = L
f(L,x) =f(L->next,x) 對應的遞歸程序如下:
當L=NULL 時
當L≠NULL 且L->data=x 時其他情況
#include "LinkList.cpp"
LinkNode*Firstxnode(LinkNode*L,intx) {if (L==NULL)return NULL;
if (L->data==x)
return L;
else
return Firstxnode(L->next,x);
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L,*p;
CreateList(L,a,n);
int x=2;
p=Firstxnode(L,x);
printf("結點值: %d\n",p->data);Release(L);
}
上述程序的執行結果如圖1.16 所示。
//包含單鏈表的基本運算算法
//返回第一個值為x的結點的地址
圖1.16
程序執行結果
15.解:設f(L,x)刪除單鏈表L 中第一個值為x 的結點,其遞歸模型如下:
f(L,x)≡ 不做任何事情
f(L,x)≡ 刪除L 結點,L=L->nextf(L,x)≡ f(L->next,x)
對應的遞歸程序如下:
當L=NULL
當L≠NULL 且L->data=x 其他情況
17




#include "LinkList.cpp"
算法設計
//包含單鏈表的基本運算算法
voidDelfirstx(LinkNode*&L,intx)//刪除單鏈表L中第一個值為x的結點{if (L==NULL)return;
if (L->data==x)
{LinkNode *p=L;
L=L->next;
free(p);
}
else
Delfirstx(L->next,x);
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L;
CreateList(L,a,n);
printf("刪除前L:"); DispList(L);
int x=2;
printf("刪除第一個值為%d的結點\n",x);
Delfirstx(L,x);
printf("刪除后L:"); DispList(L);
Release(L);
}
上述程序的執行結果如圖1.17 所示。
圖1.17
程序執行結果
16.解:設f(bt)返回二叉樹bt 中所有葉子結點值之和,其遞歸模型如下:
f(bt)=0
f(bt)=bt->data
f(bt)=f(bt->lchild)+f(bt->rchild) 對應的遞歸程序如下:#include "Btree.cpp"
intLeafSum(BTNode*bt)
{if (bt==NULL)return 0;
當bt=NULL
當bt≠NULL 且bt 結點為葉子結點其他情況
//包含二叉樹的基本運算算法
//二叉樹bt中所有葉子結點值之和
if (bt->lchild==NULL&& bt->rchild==NULL)
return bt->data;
int lsum=LeafSum(bt->lchild);
int rsum=LeafSum(bt->rchild);
return lsum+rsum;
}
voidmain()
18







第1章
概論
{
BTNode *bt;
Int a[]={5,2,3,4,1,6};//先序序列
Int b[]={2,3,5,1,4,6};//中序序列
int n=sizeof(a)/sizeof(a[0]);bt=CreateBTree(a,b,n);//由a和b構造二叉鏈bt printf("二叉樹bt:");DispBTree(bt);printf("\n"); printf("所有葉子結點值之和:%d\n",LeafSum(bt));
DestroyBTree(bt);
//銷毀樹bt
}
上述程序的執行結果如圖1.18 所示。
圖1.18
程序執行結果
17.解:設f(bt,k)返回二叉樹bt 中所有結點值大於等於k 的結點個數,其遞歸模型如下:
f(bt,k)=0
f(bt,k)=f(bt->lchild,k)+f(bt->rchild,k)+1f(bt,k)=f(bt->lchild,k)+f(bt->rchild,k)
對應的遞歸程序如下:
#include "Btree.cpp"
intNodenum(BTNode*bt,intk)
{if (bt==NULL)return 0;
int lnum=Nodenum(bt->lchild,k);
int rnum=Nodenum(bt->rchild,k);
if (bt->data>=k)
return lnum+rnum+1;
else
return lnum+rnum;
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
當bt=NULL
當bt≠NULL 且bt->data≥k其他情況
//包含二叉樹的基本運算算法//大於等於k的結點個數
//由a和b構造二叉鏈bt
printf("二叉樹bt:");DispBTree(bt);printf("\n"); int k=3;
printf("大於等於%d的結點個數:%d\n",k,Nodenum(bt,k));
DestroyBTree(bt);
//銷毀樹bt
}
上述程序的執行結果如圖1.19 所示。
19





算法設計
圖1.19
程序執行結果
18.解:設f(bt,x,h)返回二叉樹bt中x 結點的層次,其中h 表示bt 所指結點的層次,初始調用時,bt 指向根結點,h 置為1。其遞歸模型如下:
f(bt,x,h)=0
f(bt,x,h)=h
f(bt,x,h) =l
f(bt,x,h) =f(bt->rchild,x,h+1) 對應的遞歸程序如下:
當bt=NULL
當bt≠NULL 且bt->data=x當l=f(bt->lchild,x,h+1)≠0其他情況
#include "Btree.cpp"
intLevel(BTNode*bt,intx,int h) {//初始調用時:bt為根,h為1
if (bt==NULL) return0;
if (bt->data==x)
return h;
//包含二叉樹的基本運算算法//求二叉樹bt中x結點的層次
//找到x結點,返回h
else
{int l=Level(bt->lchild,x,h+1);//在左子樹中查找
if (l!=0)
return l;
else
//在左子樹中找到,返回其層次l
return Level(bt->rchild,x,h+1);//返回在右子樹的查找結果
}
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
//由a和b構造二叉鏈bt
printf("二叉樹bt:");DispBTree(bt);printf("\n"); int x=1;
printf("%d結點的層次: %d\n",x,Level(bt,x,1));
DestroyBTree(bt);
}
上述程序的執行結果如圖1.20 所示。
//銷毀樹bt
圖1.20
程序執行結果20






第1章
概論
1.3第3 章─分治法
1.3.1練習題
1.分治法的設計思想是將一個難以直接解決的大問題分割成規模較小的子問題,分別解決子問題,最后將子問題的解組合起來形成原問題的解。這要求原問題和子問題()。
A.問題規模相同,問題性質相同
B.問題規模相同,問題性質不同
C.問題規模不同,問題性質相同
D.問題規模不同,問題性質不同
2.在尋找n 個元素中第k 小元素問題中,如快速排序算法思想,運用分治算法對n個元素進行划分,如何選擇划分基准?下面()答案解釋最合理。
A.隨機選擇一個元素作為划分基准
B.取子序列的第一個元素作為划分基准
C.用中位數的中位數方法尋找划分基准
D.以上皆可行。但不同方法,算法復雜度上界可能不同
3.對於下列二分查找算法,以下正確的是()。
A.
intbinarySearch(inta[],intn,int x)
{int low=0, high=n-1;
while(low<=high)
{int mid=(low+high)/2;
if(x==a[mid]) returnmid;
if(x>a[mid]) low=mid;
else high=mid;
}
return –1;
}
B.
intbinarySearch(inta[],intn,int x)
{int low=0, high=n-1;
while(low+1!=high)
{int mid=(low+high)/2;
if(x>=a[mid]) low=mid;
else high=mid;
}
if(x==a[low]) returnlow;
else return –1;
}
C.
intbinarySearch(inta[],intn,intx)
{int low=0, high=n-1;
while(low<high-1)
{int mid=(low+high)/2;
21





算法設計
if(x<a[mid])
high=mid;
else low=mid;
}
if(x==a[low]) returnlow;
else return –1;
}
D.
intbinarySearch(inta[],intn,int x)
{if(n > 0 &&x >= a[0])
{int low = 0,high = n-1;
while(low < high)
{int mid=(low+high+1)/2;
if(x < a[mid])
high=mid-1;
else low=mid;
}
if(x==a[low]) returnlow;
}
return –1;
}
4.快速排序算法是根據分治策略來設計的,簡述其基本思想。
5.假設含有n 個元素的待排序的數據a 恰好是遞減排列的,說明調用QuickSort(a,0,n-1)遞增排序的時間復雜度為O(n)。
6.以下哪些算法采用分治策略:
(1)堆排序算法
(2)二路歸並排序算法
(3)折半查找算法
(4)順序查找算法
7.適合並行計算的問題通常表現出哪些特征?
8.設有兩個復數x=a+bi和y=c+di。復數乘積xy 可以使用4 次乘法來完成,即xy=(ac-bd)+(ad+bc)i。設計一個僅用3 次乘法來計算乘積xy 的方法。
9.有4 個數組a、b、c 和d,都已經排好序,說明找出這4 個數組的交集的方法。10.設計一個算法,采用分治法求一個整數序列中的最大最小元素。
11.設計一個算法,采用分治法求x。
12.假設二叉樹采用二叉鏈存儲結構進行存儲。設計一個算法采用分治法求一棵二叉樹bt 的高度。
13.假設二叉樹采用二叉鏈存儲結構進行存儲。設計一個算法采用分治法求一棵二叉樹bt 中度為2 的結點個數。
14.有一種二叉排序樹,其定義是空樹是一棵二叉排序樹,若不空,左子樹中所有結點值小於根結點值,右子樹中所有結點值大於根結點值,並且左右子樹都是二叉排序樹。現在該二叉排序樹采用二叉鏈存儲,采用分治法設計查找值為x 的結點地址,並分析算法的最好的平均時間復雜度。
22







第1章
概論
15.設有n 個互不相同的整數,按遞增順序存放在數組a[0..n-1]中,若存在一個下標i(0≤i<n),使得a[i]=i。設計一個算法以O(log2n)時間找到這個下標i。
16.請你模仿二分查找過程設計一個三分查找算法。分析其時間復雜度。
17.對於大於1 的正整數n,可以分解為n=x1*x2*…*xm,其中xi≥2。例如,n=12 時有8 種不同的分解式:12=12,12=6*2,12=4*3,12=3*4,12=3*2*2,12=2*6,12=2*3*2,12=2*2*3,設計一個算法求n 的不同分解式個數。
18.設計一個基於BSP 模型的並行算法,假設有p 台處理器,計算整數數組a[0..n-1]的所有元素之和。並分析算法的時間復雜度。
1.3.2練習題參考答案
1.答:C。
2.答:D。
3.答:以a[]={1,2,3,4,5}為例說明。選項A中在查找5 時出現死循環。選項B 中在查找5 時返回-1。選項C 中在查找5 時返回-1。選項D 正確。
4.答:對於無序序列a[low..high]進行快速排序,整個排序為“大問題”。選擇其中的一個基准base=a[i](通常以序列中第一個元素為基准),將所有小於等於base 的元素移動到它的前面,所有大於等於base 的元素移動到它的后面,即將基准歸位到a[i],這樣產生a[low..i-1]和a[i+1..high]兩個無序序列,它們的排序為“小問題”。當a[low..high]序列只有一個元素或者為空時對應遞歸出口。
所以快速排序算法就是采用分治策略,將一個“大問題”分解為兩個“小問題”來求解。由於元素都是在a 數組中,其合並過程是自然產生的,不需要特別設計。
5.答:此時快速排序對應的遞歸樹高度為O(n),每一次划分對應的時間為O(n),所以整個排序時間為O(n)。
6.答:其中二路歸並排序和折半查找算法采用分治策略。
7.答:適合並行計算的問題通常表現出以下特征:
(1)將工作分離成離散部分,有助於同時解決。例如,對於分治法設計的串行算法,可以將各個獨立的子問題並行求解,最后合並成整個問題的解,從而轉化為並行算法。
(2)隨時並及時地執行多個程序指令。
(3)多計算資源下解決問題的耗時要少於單個計算資源下的耗時。
8.答:xy=(ac-bd)+((a+b)(c+d)-ac-bd)i。由此可見,這樣計算xy只需要3次乘法(即ac、bd 和(a+b)(c+d)乘法運算)。
9.答:采用基本的二路歸並思路,先求出a、b 的交集ab,再求出c、d 的交集cd,最后求出ab 和cd的交集,即為最后的結果。也可以直接采用4 路歸並方法求解。
10.解:采用類似求求一個整數序列中的最大次大元素的分治法思路。對應的程序如下:
#include <stdio.h>
#define max(x,y)((x)>(y)?(x):(y))
#define min(x,y)((x)<(y)?(x):(y))
23





算法設計
voidMaxMin(inta[],intlow,inthigh,int&maxe,int&mine)
//求a中最大最小元素
{if (low==high)
{maxe=a[low];
mine=a[low];
}
else if (low==high-1)
{maxe=max(a[low],a[high]);
mine=min(a[low],a[high]);
}
else
{int mid=(low+high)/2;
//只有一個元素
//只有兩個元素
//有兩個以上元素
int lmaxe,lmine;
MaxMin(a,low,mid,lmaxe,lmine);
int rmaxe,rmine;
MaxMin(a,mid+1,high,rmaxe,rmine);
maxe=max(lmaxe,rmaxe);
mine=min(lmine,rmine);
}
}
voidmain()
{int a[]={4,3,1,2,5};
int n=sizeof(a)/sizeof(a[0]);
int maxe,mine;
MaxMin(a,0,n-1,maxe,mine);
printf("Max=%d, Min=%d\n",maxe,mine);
}
上述程序的執行結果如圖1.21 所示。
圖1.21
程序執行結果
11.解:設f(x,n)=x,采用分治法求解對應的遞歸模型如下:
f(x,n)=x
f(x,n)=f(x,n/2)*f(x,n/2)
f(x,n)=f(x,(n-1)/2)*f(x,(n-1)/2)*x對應的遞歸程序如下:#include <stdio.h>
doublesolve(doublex,intn)
{double fv;
if (n==1) returnx;
if (n%2==0)
{fv=solve(x,n/2);
return fv*fv;
}
當n=1 當n 為偶數時當n 為奇數時
//求x^n
24







第1章
else
{fv=solve(x,(n-1)/2);
return fv*fv*x;
}
}
voidmain()
{double x=2.0;
printf("求解結果:\n");
for (int i=1;i<=10;i++)
printf("%g^%d=%g\n",x,i,solve(x,i));
}
上述程序的執行結果如圖1.22 所示。
概論
圖1.22
程序執行結果
12.解:設f(bt)返回二叉樹bt 的高度,對應的遞歸模型如下:
f(bt)=0
f(bt)=MAX{f(bt->lchild),f(bt->rchild)}+1對應的程序如下:
#include "Btree.cpp"
intHeight(BTNode*bt)
{if (bt==NULL)return 0;
int lh=Height(bt->lchild);
int rh=Height(bt->rchild);
if (lh>rh) returnlh+1;
else return rh+1;
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
當bt=NULL
其他情況
//包含二叉樹的基本運算算法//求二叉樹bt的高度
//子問題1
//子問題2
//合並
//由a和b構造二叉鏈bt
printf("二叉樹bt:");DispBTree(bt);printf("\n"); printf("bt的高度:%d\n",Height(bt));
DestroyBTree(bt);
//銷毀樹bt
}
25





算法設計
上述程序的執行結果如圖1.23 所示。
圖1.23
程序執行結果
13.解:設f(bt)返回二叉樹bt 中度為2 的結點個數,對應的遞歸模型如下:
f(bt)=0
f(bt)=f(bt->lchild)+f(bt->rchild)+1f(bt)=f(bt->lchild)+f(bt->rchild)
對應的算法如下:
#include "Btree.cpp"
intNodes(BTNode*bt)
{int n=0;
當bt=NULL
若bt≠NULL 且bt 為雙分支結點其他情況
//包含二叉樹的基本運算算法//求bt中度為2的結點個數
if (bt==NULL) return0;
if (bt->lchild!=NULL&& bt->rchild!=NULL)
n=1;
return Nodes(bt->lchild)+Nodes(bt->rchild)+n;
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
//由a和b構造二叉鏈bt
printf("二叉樹bt:");DispBTree(bt);printf("\n"); printf("bt中度為2的結點個數:%d\n",Nodes(bt));
DestroyBTree(bt);
//銷毀樹bt
}
上述程序的執行結果如圖1.24 所示。
圖1.24
程序執行結果
14.解:設f(bt,x)返回在二叉排序樹bt 得到的值為x 結點的地址,若沒有找到返回空,對應的遞歸模型如下:
f(bt,x)=NULL
f(bt,x)=bt
f(bt,x)=f(bt->lchild,x)
當bt=NULL
當bt≠NULL 且x=bt->data當x>bt->data
26







第1章
概論
f(bt,x)=f(bt->rchild,x) 對應的程序如下:
#include "Btree.cpp"
當x<bt->data
//包含二叉樹的基本運算算法
BTNode*Search(BTNode*bt,Int x)//在二叉排序樹bt查找的值為x結點{if (bt==NULL)return NULL;
if (x==bt->data)return bt;
if (x<bt->data) returnSearch(bt->lchild,x);
else return Search(bt->rchild,x);
}
voidmain()
{BTNode *bt;
Int a[]={4,3,2,8,6,7,9};
Int b[]={2,3,4,6,7,8,9};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
//構造一棵二叉排序樹bt
printf("二叉排序樹bt:");DispBTree(bt); printf("\n");int x=6;
BTNode *p=Search(bt,x);
if (p!=NULL)
printf("找到結點:%d\n",p->data);
else
printf("沒有找到結點\n",x);
DestroyBTree(bt);
//銷毀樹bt
}
上述程序的執行結果如圖1.25 所示。
圖1.25
程序執行結果
Search(bt,x)算法采用的是減治法,最好的情況是某個結點左右子樹高度大致相同,其平均執行時間T(n)如下:
T(n)=1
T(n)=T(n/2)+1
當n=1
當n>1
可以推出T(n)=O(log2n),其中n 為二叉排序樹的結點個數。
15.解:采用二分查找方法。a[i]=i 時表示該元素在有序非重復序列a 中恰好第i 大。對於序列a[low..high],mid=(low+high)/2,若a[mid]=mid 表示找到該元素;若a[mid]>mid說明右區間的所有元素都大於其位置,只能在左區間中查找;若a[mid]<mid說明左區間的所有元素都小於其位置,只能在右區間中查找。對應的程序如下:
#include <stdio.h>
intSearch(inta[],intn)
{int low=0,high=n-1,mid;
//查找使得a[i]=i
27




算法設計
while (low<=high)
{mid=(low+high)/2;
if (a[mid]==mid)//查找到這樣的元素
return mid;
else if (a[mid]<mid)//這樣的元素只能在右區間中出現 low=mid+1;
else
//這樣的元素只能在左區間中出現
high=mid-1;
}
return -1;
}
voidmain()
{int a[]={-2,-1,2,4,6,8,9};
int n=sizeof(a)/sizeof(a[0]);
int i=Search(a,n);
printf("求解結果\n");
if (i!=-1)
printf("存在a[%d]=%d\n",i,i);else
printf("不存在\n");
}
上述程序的執行結果如圖1.26 所示。
圖1.26
程序執行結果
16.解:對於有序序列a[low..high],若元素個數少於3個,直接查找。若含有更多的元素,將其分為a[low..mid1-1]、a[mid1+1..mid2-1]、a[mid2+1..high]子序列,對每個子序列遞歸查找,算法的時間復雜度為O(log3n),屬於O(log2n)級別。對應的算法如下:
#include <stdio.h>
intSearch(inta[],intlow,inthigh,intx) {if (high<low)
return -1;
else if (high==low)
{if (x==a[low])
return low;
else
return -1;
}
if (high-low<2)
{if (x==a[low])
return low;
else if (x==a[low+1])
return low+1;
else
//三分查找
//序列中沒有元素
//序列中只有1個元素
//序列中只有2個元素
28







第1章
概論
return -1;
}
int length=(high-low+1)/3;
int mid1=low+length;
int mid2=high-length;
if (x==a[mid1])
return mid1;
else if (x<a[mid1])
return Search(a,low,mid1-1,x);
else if (x==a[mid2])
return mid2;
else if (x<a[mid2])
return Search(a,mid1+1,mid2-1,x);
else
return Search(a,mid2+1,high,x);
}
voidmain()
{int a[]={1,3,5,7,9,11,13,15};
int n=sizeof(a)/sizeof(a[0]);printf("求解結果\n");
int x=13;
int i=Search(a,0,n-1,x);
if (i!=-1)
printf("a[%d]=%d\n",i,x);
else
printf("不存在%d\n",x);
int y=10;
int j=Search(a,0,n-1,y);
if (j!=-1)
printf("a[%d]=%d\n",j,y);
else
printf("不存在%d\n",y);
}
上述程序的執行結果如圖1.27 所示。
//每個子序列的長度
圖1.27
程序執行結果
17.解:設f(n)表示n的不同分解式個數。有:f(1)=1,作為遞歸出口
f(2)=1,分解式為:2=2
f(3)=1,分解式為:3=3
f(4)=2,分解式為:4=4,4=2*2
29





算法設計
f(6)=3,分解式為:6=6,6=2*3,6=3*2,即f(6)=f(1)+f(2)+f(3)
以此類推,可以看出f(n)為n 的所有因數的不同分解式個數之和,即f(n)= ∑𝑓(𝑛/𝑖)。對應的程序如下:
#include <stdio.h>
#define MAX 101
intsolve(int n)
{if (n==1) return1; else
{int sum=0;
//求n的不同分解式個數
for (int i=2;i<=n;i++)
if (n%i==0)
sum+=solve(n/i);
return sum;
}
}
voidmain()
{int n=12;
int ans=solve(n);
printf("結果: %d\n",ans);
}
上述程序的執行結果如圖1.28 所示。
圖1.28
程序執行結果
18.解:對應的並行算法如下:
intSum(inta[],ints,intt,intp,int i)//處理器i執行求和{int j,s=0;
for (j=s;j<=t;j++)
s+=a[j];
return s;
}
intParaSum(inta[],ints,intt,intp,int i)
{int sum=0,j,k=0,sj;
for (j=0;j<p;j++)
{sj=Sum(a,k,k+n/p-1,p,j);
k+=n/p;
}
sum+=sj;
return sum;
//for循環的各個子問題並行執行
}
每個處理器的執行時間為O(n/p),同步開銷為O(p),所以該算法的時間復雜度為O(n/p+p)。
30






第1章
概論
1.4第4 章─蠻力法
1.4.1練習題
1.簡要比較蠻力法和分治法。
2.在采用蠻力法求解時什么情況下使用遞歸?
3.考慮下面這個算法,它求的是數組a 中大小相差最小的兩個元素的差。請對這個算法做盡可能多的改進。
#define INF 99999
#define abs(x) (x)<0?-(x):(x)
intMindif(inta[],intn)
{int dmin=INF;
for (int i=0;i<=n-2;i++)
//求絕對值宏
for (int j=i+1;j<=n-1;j++)
{int temp=abs(a[i]-a[j]);
if (temp<dmin)
dmin=temp;
}
return dmin;
}
4.給定一個整數數組A=(a0,a1,…an-1),若i<j且ai>aj,則<ai,aj>就為一個逆序對。例如數組(3,1,4,5,2)的逆序對有<3,1>,<3,2>,<4,2>,<5,2>。設計一個算法采用蠻力法求A 中逆序對的個數即逆序數。
5.對於給定的正整數n(n>1),采用蠻力法求1!+2!+…+n!,並改進該算法提高效率。
6.有一群雞和一群兔,它們的只數相同,它們的腳數都是三位數,且這兩個三位數的各位數字只能是0、1、2、3、4、5。設計一個算法用蠻力法求雞和兔的只數各是多少?它們的腳數各是多少?
7.有一個三位數,個位數字比百位數字大,而百位數字又比十位數字大,並且各位數字之和等於各位數字相乘之積,設計一個算法用窮舉法求此三位數。
8.某年級的同學集體去公園划船,如果每只船坐10 人,那么多出2 個座位;如果每只船多坐2 人,那么可少租1 只船,設計一個算法用蠻力法求該年級的最多人數?
9.已知:若一個合數的質因數分解式逐位相加之和等於其本身逐位相加之和,則稱這個數為Smith 數。如4937775=3*5*5*65837,而3+5+5+6+5+8+3+7=42,4+9+3+7+7+7+5=42,所以4937775 是Smith 數。求給定一個正整數N,求大於N 的最小Smith 數。
輸入:若干個case,每個case 一行代表正整數N,輸入0 表示結束
輸出:大於N 的最小Smith數
輸入樣例:
4937774
0
樣例輸出:
31




算法設計
4937775
10.求解塗棋盤問題。小易有一塊n*n 的棋盤,棋盤的每一個格子都為黑色或者白色,小易現在要用他喜歡的紅色去塗畫棋盤。小易會找出棋盤中某一列中擁有相同顏色的最大的區域去塗畫,幫助小易算算他會塗畫多少個棋格。
輸入描述:輸入數據包括n+1 行:第一行為一個整數n(1≤n≤50),即棋盤的大小,接下來的n 行每行一個字符串表示第i 行棋盤的顏色,'W'表示白色,'B'表示黑色。
輸出描述:輸出小易會塗畫的區域大小。
輸入例子:
3
BWW
BBB
BWB
輸出例子:
3
11.給定一個含n(n>1)個整數元素的a,所有元素不相同,采用蠻力法求出a 中所有元素的全排列。
1.4.2練習題參考答案
1.答:蠻力法是一種簡單直接地解決問題的方法,適用范圍廣,是能解決幾乎所有問題的一般性方法,常用於一些非常基本、但又十分重要的算法(排序、查找、矩陣乘法和字符串匹配等),蠻力法主要解決一些規模小或價值低的問題,可以作為同樣問題的更高效算法的一個標准。而分治法采用分而治之思路,把一個復雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題直到問題解決。分治法在求解問題時,通常性能比蠻力法好。
2.答:如果用蠻力法求解的問題可以分解為若干個規模較小的相似子問題,此時可以采用遞歸來實現算法。
3.解:上述算法的時間復雜度為O(n),采用的是最基本的蠻力法。可以先對a中元素遞增排序,然后依次比較相鄰元素的差,求出最小差,改進后的算法如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
intMindif1(inta[],intn)
{sort(a,a+n);
int dmin=a[1]-a[0];
for (int i=2;i<n;i++)
{int temp=a[i]-a[i-1];
if (temp<dmin)
dmin=temp;
}
return dmin;
}
//遞增排序
32






第1章
概論
上述算法的主要時間花費在排序上,算法的時間復雜度為O(nlog2n)。
4.解:采用兩重循環直接判斷是否為逆序對,算法的時間復雜度為O(n2),比第3章實驗3 算法的性能差。對應的算法如下:
intsolve(inta[],intn)
{int ans=0;
for (int i=0;i<n-1;i++)
for (int j=i+1;j<n;j++)
if (a[i]>a[j])
ans++;
return ans;
//求逆序數
}
5.解:直接采用蠻力法求解算法如下:
longf(intn)
{long fn=1;
for (int i=2;i<=n;i++)
fn=fn*i;
return fn;
}
longsolve(intn)
{long ans=0;
for (int i=1;i<=n;i++)
ans+=f(i);
return ans;
//求n!
//求1!+2!+…+n!
}
實際上,f(n)=f(n-1)*n,f(1)=1,在求f(n)時可以利用f(n-1)的結果。改進后的算法如
下:
longsolve1(intn)
{long ans=0;
long fn=1;
for (int i=1;i<=n;i++)
{fn=fn*i;
ans+=fn;
}
return ans;
//求1!+2!+…+n!
}
6.解:設雞腳數為y=abc,兔腳數為z=def,有1≤a,d≤5,0≤b,c,e,f≤5,采用6 重循環,求出雞只數x1=y/2(y 是2的倍數),兔只數x2=z/4(z 是4 的倍數),當x1=x2 時輸出結果。對應的程序如下:
#include <stdio.h>
voidsolve()
{int a,b,c,d,e,f;
int x1,x2,y,z;
for (a=1;a<=5;a++)
for (b=0;b<=5;b++)
for (c=0;c<=5;c++)
33




算法設計
for (d=1;d<=5;d++)
for (e=0;e<=5;e++)
for (f=0;f<=5;f++)
{y=a*100+b*10+c;
z=d*100+e*10+f;
if (y%2!=0 || z%4!=0)
continue;
x1=y/2;
x2=z/4;
if (x1==x2)
//雞腳數
//兔腳數
//雞只數
//兔只數
printf("雞只數:%d,兔只數:%d,雞腳數:%d, 兔腳數:%d\n",x1,x2,y,z);
}
}
voidmain()
{printf("求解結果\n");
solve();
}
上述程序的執行結果如圖1.29 所示。
圖1.29程序執行結果
7.解:設該三位數為x=abc,有1≤a≤9,0≤b,c≤9,滿足c>a,a>b,a+b+c=a*b*c。對應的程序如下:
#include <stdio.h>
voidsolve()
{int a,b,c;
for (a=1;a<=9;a++)
for (b=0;b<=9;b++)
for (c=0;c<=9;c++)
{if (c>a && a>b&&a+b+c==a*b*c)
printf("%d%d%d\n",a,b,c);
}
}
voidmain()
34








第1章
概論
{
printf("求解結果\n");solve();
}
上述程序的執行結果如圖1.30 所示。
圖1.30程序執行結果
8.解:設該年級的人數為x,租船數為y。因為每只船坐10 人正好多出2 個座位,則x=10*y-2;因為每只船多坐2 人即12 人時可少租1 只船(沒有說恰好全部座位占滿),有x+z=12*(y-1),z 表示此時空出的座位,顯然z<12。讓y 從1 到100(實際上y取更大范圍的結果是相同的)、z從0到11 枚舉,求出最大的x 即可。對應的程序如下:
#include <stdio.h>
intsolve()
{int x,y,z;
for (y=1;y<=100;y++)
for (z=0;z<12;z++)
if (10*y-2==12*(y-1)-z)
x=10*y-2;
return x;
}
voidmain()
{printf("求解結果\n");
printf("最多人數:%d\n",solve());
}
上述程序的執行結果如圖1.31 所示。
圖1.31程序執行結果
9.解:采用蠻力法求出一個正整數n的各位數字和sum1,以及n的所有質因數的數字和sum2,若sum1=sum2,即為Smitch數。從用戶輸入的n開始枚舉,若是Smitch 數,輸出,本次結束,否則n++繼續查找大於n 的最小Smitch數。對應的完整程序如下:
#include <stdio.h>
intSum(intn)
{int sum=0;
while (n>0)
//求n的各位數字和
35



算法設計
{sum+=n%10;
n=n/10;
}
return sum;
}
boolsolve(intn)//判斷n是否為Smitch數
{int m=2;
int sum1=Sum(n);
int sum2=0;
while (n>=m)
{if (n%m==0)//找到一個質因數m
{n=n/m;
sum2+=Sum(m);
}
else
m++;
}
if (sum1==sum2)
return true;
else
return false;
}
voidmain()
{int n;
while (true)
{scanf("%d",&n);
if (n==0) break;
while (!solve(n))
n++;
printf("%d\n",n);
}
}
10.解:采用蠻力法,統計每一列相鄰相同顏色的棋格個數countj,在countj中求最大值。對應的程序如下:
#include <stdio.h>
#define MAXN 51
//問題表示
int n;
char board[MAXN][MAXN];
intgetMaxArea()
{int maxArea=0;
for (int j=0; j<n;j++){int countj=1;
//蠻力法求解算法
for (int i=1; i<n;i++)//統計第j列中相同顏色相鄰棋格個數 {if (board[i][j]==board[i-1][j])
countj++;
else
countj=1;
}
36






第1章
概論
if (countj>maxArea)
maxArea=countj;
}
return maxArea;
}
intmain()
{scanf("%d",&n);
for (int i=0;i<n;i++)
scanf("%s",board[i]);
printf("%d\n",getMaxArea());
return 0;
}
11.解:與《教程》中求全排列類似,但需要將求1~n 的全排列改為按下標0~n-1 求a 的全排列(下標從0 開始)。采用非遞歸的程序如下:
#include <stdio.h>
#include <vector>
using namespace std;
vector<vector<int>> ps;
//存放全排列
voidInsert(vector<int>s,inta[],inti,vector<vector<int>> &ps1)//在每個集合元素中間插入i得到ps1
{vector<int>s1;
vector<int>::iteratorit;
for (int j=0;j<=i;j++)
{s1=s;
it=s1.begin()+j;
s1.insert(it,a[i]);
ps1.push_back(s1);
}
}
voidPerm(inta[],intn)
{vector<vector<int>> ps1;
vector<vector<int>>::iterator it;
vector<int> s,s1;
s.push_back(a[0]);
ps.push_back(s);
for (int i=1;i<n;i++)
{ps1.clear();
for (it=ps.begin();it!=ps.end();++it)
Insert(*it,a,i,ps1);
ps=ps1;
}
}
voiddispps()
//在s(含i個整數)的每個位置插入a[i] //求出插入位置
//插入整數a[i]
//添加到ps1中
//求a[0..n-1]的所有全排列
//臨時存放子排列
//全排列迭代器
//添加{a[0]}集合元素
//循環添加a[1]~a[n-1]
//ps1存放插入a[i]的結果
//在每個集合元素中間插入a[i]得到ps1 //輸出全排列ps
{vector<vector<int>>::reverse_iteratorit;//全排列的反向迭代器
vector<int>::iteratorsit;
//排列集合元素迭代器
for (it=ps.rbegin();it!=ps.rend();++it)
{for (sit=(*it).begin();sit!=(*it).end();++sit)
printf("%d",*sit);
printf("");
37




}
printf("\n");
}
voidmain()
{int a[]={2,5,8};
int n=sizeof(a)/sizeof(a[0]);printf("a[0~%d]的全排序如下:\nPerm(a,n);
dispps();
算法設計
",n-1);
}
上述程序的執行結果如圖1.32 所示。
圖1.32程序執行結果
1.5第5 章─回溯法
1.5.1練習題
1.回溯法在問題的解空間樹中,按()策略,從根結點出發搜索解空間樹。
A.廣度優先B.活結點優先C.擴展結點優先D.深度優先
2.關於回溯法以下敘述中不正確的是()。
A.回溯法有“通用解題法”之稱,它可以系統地搜索一個問題的所有解或任意解
B.回溯法是一種既帶系統性又帶有跳躍性的搜索算法
C.回溯算法需要借助隊列這種結構來保存從根結點到當前擴展結點的路徑
D.回溯算法在生成解空間的任一結點時,先判斷該結點是否可能包含問題的解,如果肯定不包含,則跳過對該結點為根的子樹的搜索,逐層向祖先結點回溯
3.回溯法的效率不依賴於下列哪些因素()。
A.確定解空間的時間C.計算約束函數的時間
B.滿足顯約束的值的個數D.計算限界函數的時間
4.下面()函數是回溯法中為避免無效搜索采取的策略。
A.遞歸函數B.剪枝函數C.隨機數函數D.搜索函數
5.回溯法的搜索特點是什么?
6.用回溯法解0/1 背包問題時,該問題的解空間是何種結構?用回溯法解流水作業調度問題時,該問題的解空間是何種結構?
7.對於遞增序列a[]={1,2,3,4,5},采用例5.4的回溯法求全排列,以1、2開頭的排列一定最先出現嗎?為什么?
8.考慮n 皇后問題,其解空間樹為由1、2、…、n 構成的n!種排列所組成。現用回
38






第1章
概論
溯法求解,要求:
(1)通過解搜索空間說明n=3 時是無解的。
(2)給出剪枝操作。
(3)最壞情況下在解空間樹上會生成多少個結點?分析算法的時間復雜度。9.設計一個算法求解簡單裝載問題,設有一批集裝箱要裝上一艘載重量為W 的輪
船,其中編號為i(0≤i≤n-1)的集裝箱的重量為wi。現要從n 個集裝箱中選出若干裝上輪船,使它們的重量之和正好為W。如果找到任一種解返回true,否則返回false。
10.給定若干個正整數a0、a0 要求找選擇元素個數最少的解。
、…、an-1
,從中選出若干數,使它們的和恰好為k,
11.設計求解有重復元素的排列問題的算法,設有n個元素a[]={a0,a1,…,an-1),其中可能含有重復的元素,求這些元素的所有不同排列。如a[]={1,1,2},輸出結果是(1,1,2),(1,2,1),(2,1,1)。
12.采用遞歸回溯法設計一個算法求1~n的n個整數中取出m個元素的排列,要求每個元素最多只能取一次。例如,n=3,m=2的輸出結果是(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)。
13.對於n皇后問題,有人認為當n為偶數時,其解具有對稱性,即n皇后問題的解個數恰好為n/2皇后問題的解個數的2倍,這個結論正確嗎?請編寫回溯法程序對n=4、6、8、10的情況進行驗證。
14.給定一個無向圖,由指定的起點前往指定的終點,途中經過所有其他頂點且只經過一次,稱為哈密頓路徑,閉合的哈密頓路徑稱作哈密頓回路(Hamiltoniancycle)。設計一個回溯算法求無向圖的所有哈密頓回路。
1.5.2練習題參考答案
1.答:D。
2.答:回溯算法是采用深度優先遍歷的,需要借助系統棧結構來保存從根結點到當前擴展結點的路徑。答案為C。
3.答:回溯法解空間是虛擬的,不必確定整個解空間。答案為A。
4.答:B。
5.答:回溯法在解空間樹中采用深度優先遍歷方式進行解搜索,即用約束條件和限界函數考察解向量元素x[i]的取值,如果x[i]是合理的就搜索x[i]為根結點的子樹,如果x[i]取完了所有的值,便回溯到x[i-1]。
6.答:用回溯法解0/1 背包問題時,該問題的解空間是子集樹結構。用回溯法解流水作業調度問題時,該問題的解空間是排列樹結構。
7.答:是的。對應的解空間是一棵排列樹,如圖1.33所示給出前面3 層部分,顯然最先產生的排列是從G 結點擴展出來的葉子結點,它們就是以1、2 開頭的排列。
39





















算法設計
A
1
2
3
4
5
B
C
D
E
F
2
3
4
5
G
H
I
圖1.33
J
部分解空間樹
8.答:(1)n=3 時的解搜索空間如圖1.34所示,不能得到任何葉子結點,所有無解。
(2)剪枝操作是任何兩個皇后不能同行、同列和同兩條對角線。
(3)最壞情況下每個結點擴展n 個結點,共有n個結點,算法的時間復雜度為O(n)。
(*,*,*)
(1,*,*)
(1,3,*)
(2,*,*)
(3,*,*)
(3,1,*)
圖1.34
3 皇后問題的解搜索空間
9.解:用數組w[0..n-1]存放n 個集裝箱的重量,采用類似判斷子集和是否存在解的方法求解。對應完整的求解程序如下:
#include <stdio.h>
#define MAXN 20
//問題表示
int n=5,W;
int w[]={2,9,5,6,3};
int count;
voiddfs(inttw,intrw,inti) {if (i>=n)
{if (tw==W)
count++;
}
else
//最多集裝箱個數
//全局變量,累計解個數
//求解簡單裝載問題
//找到一個葉子結點
//找到一個滿足條件的解,輸出它//尚未找完
{
}
rw-=w[i];
if (tw+w[i]<=W)
dfs(tw+w[i],rw,i+1);
if (tw+rw>=W)
dfs(tw,rw,i+1);
//求剩余的集裝箱重量和
//左孩子結點剪枝:選取滿足條件的集裝箱w[i] //選取第i個集裝箱
//右孩子結點剪枝:剪除不可能存在解的結點 //不選取第i個集裝箱,回溯
}
boolsolve()
//判斷簡單裝載問題是否存在解40







第1章
概論
{count=0;
int rw=0;
for (int j=0;j<n;j++)
rw+=w[j];
dfs(0,rw,0);
if (count>0)
return true;
else
return false;
}
voidmain()
{printf("求解結果\n");
//求所有集裝箱重量和rw//i從0開始
W=4;
printf("W=%d時%s\n",W,(solve()?"存在解":"沒有解"));W=10;
printf("W=%d時%s\n",W,(solve()?"存在解":"沒有解"));W=12;
printf("W=%d時%s\n",W,(solve()?"存在解":"沒有解"));W=21;
printf("W=%d時%s\n",W,(solve()?"存在解":"沒有解"));}
本程序執行結果如圖1.35 所示。
圖1.35
程序執行結果
10.解:這是一個典型的解空間為子集樹的問題,采用子集樹的回溯算法框架。當找到一個解后通過選取的元素個數進行比較求最優解minpath。對應的完整程序如下:
#include <stdio.h>
#include <vector>
using namespace std;
//問題表示
int a[]={1,2,3,4,5};
int n=5,k=9;
vector<int> minpath;
//求解結果表示
int minn=n;
voiddisppath()
{printf("選擇的元素:");
//設置為全局變量 //存放最優解
//最多選擇n個元素//輸出一個解
for (int j=0;j<minpath.size();j++)
printf("%d ",minpath[j]);printf("元素個數=%d\n",minn);
}
41




算法設計
voiddfs(vector<int>path,intsum,intstart)//求解算法
{if (sum==k)
{if (path.size()<minn)
{minn=path.size();
minpath=path;
}
return;
}
if (start>=n) return;
dfs(path,sum,start+1);
//如果找到一個解,不一定到葉子結點 //全部元素找完,返回
//不選擇a[start]
path.push_back(a[start]);//選擇a[start] dfs(path,sum+a[start],start+1);
}
voidmain()
{vector<int>path;
dfs(path,0,0); printf("最優解:\n");disppath();
//path存放一個子集
}
上述程序的執行結果如圖1.36 所示。
圖1.36
程序執行結果
11.解:在回溯法求全排列的基礎上,增加元素的重復性判斷。例如,對於a[]={1,1,2},不判斷重復性時輸出(1,1,2),(1,2,1),(1,1,2),(1,2,1),(2,1,1),(2,1,1),共6個,有3 個是重復的。重復性判斷是這樣的,對於在擴展a[i]時,僅僅將與a[i..j-1]沒有出現的元素a[j]交換到a[i]的位置,如果出現,對應的排列已經在前面求出了。對應的完整程序如下:
#include <stdio.h>
boolok(inta[],inti,intj)//ok用於判別重復元素
{if (j>i)
{for(int k=i;k<j;k++)
if (a[k]==a[j])
return false;
}
return true;
}
voidswap(int&x,int&y)
{int tmp=x;
x=y; y=tmp;
//交換兩個元素
}
voiddfs(inta[],intn,inti)//求有重復元素的排列問題{if (i==n)
42







第1章
概論
{for(int j=0;j<n;j++)
printf("%3d",a[j]);
printf("\n");
}
else
{for (int j=i;j<n;j++)
if (ok(a,i,j))//選取與a[i..j-1]不重復的元素a[j] {swap(a[i],a[j]);
dfs(a,n,i+1);
swap(a[i],a[j]);
}
}
}
voidmain()
{int a[]={1,2,1,2};
int n=sizeof(a)/sizeof(a[0]);
printf("序列(");
for (int i=0;i<n-1;i++)
printf("%d ",a[i]);
printf("%d)的所有不同排列:\n",a[n-1]);
dfs(a,n,0);
}
上述程序的執行結果如圖1.37 所示。
圖1.37
程序執行結果
12.解:采用求全排列的遞歸框架。選取的元素個數用i 表示(i 從1 開始),當i>m時達到一個葉子結點,輸出一個排列。為了避免重復,用used 數組實現,used[i]=0表示沒有選擇整數i,used[i]=1 表示已經選擇整數i。對應的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAXN 20
#define MAXM 10
int m,n;
int x[MAXM];
bool used[MAXN];
voiddfs(inti)
{if (i>m)
{for (int j=1;j<=m;j++)
printf("%d",x[j]);
printf("\n");
//x[1..m]存放一個排列
//求n個元素中m個元素的全排列//輸出一個排列
43




算法設計
}
else
{for (int j=1;j<=n;j++)
{if (!used[j])
{used[j]=true;
x[i]=j;
dfs(i+1);
used[j]=false;
}
}
}
}
voidmain()
{n=4,m=2;
memset(used,0,sizeof(used));printf("n=%d,m=%d的求解結果\n",n,m); dfs(1);
}
上述程序的執行結果如圖1.38 所示。
//修改used[i]
//x[i]選擇j
//繼續搜索排列的下一個元素//回溯:恢復used[i]
//初始化為0
//i從1開始
圖1.38
程序執行結果
13.解:這個結論不正確。驗證程序如下:#include <stdio.h>
#include <stdlib.h>
#define MAXN 10
int q[MAXN];
bool place(int i)
{int j=1;
if (i==1) returntrue;
while (j<i)
//測試第i行的q[i]列上能否擺放皇后 //j=1~i-1是已放置了皇后的行
{if ((q[j]==q[i])|| (abs(q[j]-q[i])==abs(j-i)))
//該皇后是否與以前皇后同列,位置(j,q[j])與(i,q[i])是否同對角線 return false;
j++;
}
return true;
}
44







第1章
概論
int Queens(int n)
{int count=0,k;
int i=1;
q[1]=0;
while (i>0)
{q[i]++;
//求n皇后問題的解個數//計數器初始化
//i為當前行
//q[i]為皇后i的列號//移到下一列
while (q[i]<=n &&!place(i))
q[i]++;
if (q[i]<=n)
{if (i==n)
count++;//找到一個解計數器count加1 else
{
i++;; q[i]=0;
}
}
else i--;
}
return count;
}
void main()
{printf("驗證結果如下:\n");for (int n=4;n<=10;n+=2)
//回溯
if (Queens(n)==2*Queens(n/2))
printf("n=%d: 正確\n",n);
else
printf("n=%d: 錯誤\n",n);
}
上述程序的執行結果如圖1.39所示。從執行結果看出結論是不正確的。
圖1.39
程序執行結果
14.解:假設給定的無向圖有n個頂點(頂點編號從0 到n-1),采用鄰接矩陣數組a(0/1 矩陣)存放,求從頂點v出發回到頂點v 的哈密頓回路。采用回溯法,解向量為x[0..n],x[i]表示第i步找到的頂點編號(i=n-1 時表示除了起點v 外其他頂點都查找了),初始時將起點v 存放到x[0],i 從1 開始查找,i>0時循環:為x[i]找到一個合適的頂點,當i=n-1 時,若頂點x[i]到頂點v有邊對應一個解;否則繼續查找下一個頂點。如果不能為x[i]找到一個合適的頂點,則回溯。采用非遞歸回溯框架(與《教程》中求解n 皇后問題的非遞歸回溯框架類似)的完整程序如下:
#include <stdio.h>
#define MAXV 10
45



//求解問題表示 int n=5;
算法設計
//圖中頂點個數
int a[MAXV][MAXV]={{0,1,1,1,0},{1,0,0,1,1},{1,0,0,0,1},{1,1,0,0,1},{0,1,1,1,0}};
//鄰接矩陣數組
//求解結果表示
int x[MAXV];
int count;
voiddispasolution()
{for (int i=0;i<=n-1;i++)
//輸出一個解路徑
printf("(%d,%d) ",x[i],x[i+1]);
printf("\n");
}
boolvalid(inti)
//判斷頂點第i個頂點x[i]的有效性
{if (a[x[i-1]][x[i]]!=1)//x[i-1]到x[i]沒有邊,返回false
return false;
for (int j=0;j<=i-1;j++)
if (x[i]==x[j])//頂點i重復出現,返回falsereturn false;
return true;
}
voidHamiltonian(intv)
{x[0]=v;
int i=1;
x[i]=-1;
while (i>0)
{x[i]++;
//求從頂點v出發的哈密頓回路//存放起點
//從頂點-1+1=0開始試探
//尚未回溯到頭,循環
while (!valid(i)&& x[i]<n)
x[i]++;
if (x[i]<n)
//試探一個頂點x[i]
//找到一個有效的頂點x[i]
{if (i==n-1)//達到葉子結點
{if (a[x[i]][v]==1)
{x[n]=v;//找到一個解
printf("第%d個解:",count++);dispasolution();
}
}
else
{
i++; x[i]=-1;
}
}
else
i--;
//回溯
}
}
void main()
{printf("求解結果\n");
for (int v=0;v<n;v++)
{printf("從頂點%d出發的哈密頓回路:\n",v); count=1;
46












第1章
概論
Hamiltonian(v);
//從頂點v出發
}
}
上述程序對如圖1.40所示的無向圖求從每個頂點出發的哈密頓回路,程序執行結果如圖1.41 所示。
1
0
3
2
4
圖1.40一個無向圖
圖1.41
程序執行結果
1.6第6 章─分枝限界法
1.6.1練習題
1.分枝限界法在問題的解空間樹中,按()策略,從根結點出發搜索解空間樹。A.廣度優先B.活結點優先C.擴展結點優先D.深度優先
2.常見的兩種分枝限界法為()。
A.廣度優先分枝限界法與深度優先分枝限界法
47



算法設計
B.隊列式(FIFO)分枝限界法與堆棧式分枝限界法
C.排列樹法與子集樹法
D.隊列式(FIFO)分枝限界法與優先隊列式分枝限界法
3.分枝限界法求解0/1 背包問題時,活結點表的組織形式是()。
A.小根堆B.大根堆C.棧
4.采用最大效益優先搜索方式的算法是()。A.分支界限法B.動態規划法C.貪心法
D.數組
D.回溯法
5.優先隊列式分枝限界法選取擴展結點的原則是()。
A.先進先出B.后進先出C.結點的優先級D.隨機
6.簡述分枝限界法的搜索策略。
7.有一個0/1 背包問題,其中n=4,物品重量為(4,7,5,3),物品價值為(40,42,25,12),背包最大載重量W=10,給出采用優先隊列式分枝限界法求最優解的過程。
8.有一個流水作業調度問題,n=4,a[]={5,10,9,7},b[]={7,5,9,8},給出采用優先隊列式分枝限界法求一個解的過程。
9.有一個含n個頂點(頂點編號為0~n-1)的帶權圖,采用鄰接矩陣數組A表示,采用分枝限界法求從起點s 到目標點t 的最短路徑長度,以及具有最短路徑長度的路徑條數。
10.采用優先隊列式分枝限界法求解最優裝載問題。給出以下裝載問題的求解過程和結果:n=5,集裝箱重量為w=(5,2,6,4,3),限重為W=10。在裝載重量相同時,最優裝載方案是集裝箱個數最少的方案。
1.6.2練習題參考答案
1.答:A。
2.答:D。
3.答:B。
4.答:A。
5.答:C。
6.答:分枝限界法的搜索策略是廣度優先遍歷,通過限界函數可以快速找到一個解或者最優解。
7.答:求解過程如下:
(1)根結點1 進隊,對應結點值:e.i=0,e.w=0,e.v=0,e.ub=76,x:[0,0,0,0]。(2)出隊結點1:左孩子結點2 進隊,對應結點值:e.no=2,e.i=1,e.w=4,
e.v=40,e.ub=76,x:[1,0,0,0];右孩子結點3進隊,對應結點值:e.no=3,e.i=1,e.w=0,e.v=0,e.ub=57,x:[0,0,0,0]。
(3)出隊結點2:左孩子超重;右孩子結點4 進隊,對應結點值:e.no=4,e.i=2,e.w=4,e.v=40,e.ub=69,x:[1,0,0,0]。
(4)出隊結點4:左孩子結點5 進隊,對應結點值:e.no=5,e.i=3,e.w=9,e.v=65,e.ub=69,x:[1,0,1,0];右孩子結點6進隊,對應結點值:e.no=6,e.i=3,e.w=4,e.v=40,e.ub=52,x:[1,0,0,0]。
48






第1章
概論
(5)出隊結點5:產生一個解,maxv= 65,bestx:[1,0,1,0]。
(6)出隊結點3:左孩子結點8 進隊,對應結點值:e.no=8,e.i=2,e.w=7,e.v=42,e.ub=57,x:[0,1,0,0];右孩子結點9被剪枝。
(7)出隊結點8:左孩子超重;右孩子結點10 被剪枝。
(8)出隊結點6:左孩子結點11 超重;右孩子結點12 被剪枝。
(9)隊列空,算法結束,產生的最優解:maxv= 65,bestx:[1,0,1,0]。
8.答:求解過程如下:
(1)根結點1 進隊,對應結點值:e.i=0,e.f1=0,e.f2=0,e.lb=29,x:[0,0,0,0]。
(2)出隊結點1:擴展結點如下:
進隊(j=1):結點2,e.i=1,e.f1=5,e.f2=12,e.lb=27,x:[1,0,0,0]。
進隊(j=2):結點3,e.i=1,e.f1=10,e.f2=15,e.lb=34,x:[2,0,0,0]。進隊(j=3):結點4,e.i=1,e.f1=9,e.f2=18,e.lb=29,x:[3,0,0,0]。
進隊(j=4):結點5,e.i=1,e.f1=7,e.f2=15,e.lb=28,x:[4,0,0,0]。
(3)出隊結點2:擴展結點如下:
進隊(j=2):結點6,e.i=2,e.f1=15,e.f2=20,e.lb=32,x:[1,2,0,0]。進隊(j=3):結點7,e.i=2,e.f1=14,e.f2=23,e.lb=27,x:[1,3,0,0]。進隊(j=4):結點8,e.i=2,e.f1=12,e.f2=20,e.lb=26,x:[1,4,0,0]。(4)出隊結點8:擴展結點如下:
進隊(j=2):結點9,e.i=3,e.f1=22,e.f2=27,e.lb=31,x:[1,4,2,0]。進隊(j=3):結點10,e.i=3,e.f1=21,e.f2=30,e.lb=26,x:[1,4,3,0]。(5)出隊結點10,擴展一個j=2的子結點,有e.i=4,到達葉子結點,產生的一個解
是e.f1=31,e.f2=36,e.lb=31,x=[1,4,3,2]。
該解對應的調度方案是:第1 步執行作業1,第2 步執行作業4,第3步執行作業3,第4 步執行作業2,總時間=36。
9.解:采用優先隊列式分枝限界法求解,隊列中結點的類型如下:
struct NodeType
{int vno;
int length;
bool operator<(constNodeType &s)const {return length>s.length;}
//頂點的編號
//當前結點的路徑長度//重載<關系函數 //length越小越優先
};
從頂點s 開始廣度優先搜索,找到目標點t 后比較求最短路徑長度及其路徑條數。對應的完整程序如下:
#include <stdio.h>
#include <queue>
using namespacestd;
#define MAX 11
#define INF 0x3f3f3f3f
//問題表示
int A[MAX][MAX]={
//一個帶權有向圖
49



{0,1,4,INF,INF},
{INF,0,INF,1,5},
{INF,INF,0,INF,1},{INF,INF,2,0,3},
{INF,INF,INF,INF,INF} };
int n=5;
//求解結果表示
int bestlen=INF;
int bestcount=0;
struct NodeType
{int vno;
int length;
算法設計
//最優路徑的路徑長度//最優路徑的條數
//頂點的編號
//當前結點的路徑長度
bool operator<(constNodeType &s)const//重載>關系函數
{return length>s.length;}
};
voidsolve(ints,intt)
{NodeType e,e1;priority_queue<NodeType>qu;
e.vno=s;
e.length=0;
qu.push(e);
while (!qu.empty())
{e=qu.top();qu.pop();
if (e.vno==t)
{if (e.length<bestlen)
{bestcount=1;
bestlen=e.length;
}
else if (e.length==bestlen)
bestcount++;
}
else
{for (int j=0;j<n;j++)
//length越小越優先
//求最短路徑問題
//定義2個結點
//定義一個優先隊列qu//構造根結點
//根結點進隊
//隊不空循環
//出隊結點e作為當前結點//e是一個葉子結點
//比較找最優解
//保存最短路徑長度
//e不是葉子結點
//檢查e的所有相鄰頂點
if (A[e.vno][j]!=INF&& A[e.vno][j]!=0)//頂點e.vno到頂點j有邊{if (e.length+A[e.vno][j]<bestlen)//剪枝
{e1.vno=j;
e1.length=e.length+A[e.vno][j];
qu.push(e1);
}
}
}
}
}
voidmain()
{int s=0,t=4;
solve(s,t);
if (bestcount==0)
printf("頂點%d到%d沒有路徑\n",s,t); else
{printf("頂點%d到%d存在路徑\n",s,t);
50
//有效子結點e1進隊







第1章
概論
printf("最短路徑長度=%d,條數=%d\n", bestlen,bestcount);//輸出:5 3
}
}
上述程序的執行結果如圖1.39 所示。
圖1.39
程序執行結果
10.解:采用優先隊列式分枝限界法求解。設計優先隊列priority_queue<NodeType>,並設計優先隊列的關系比較函數Cmp,指定按結點的ub值進行比較,即ub 值越大的結點越先出隊。對應的完整程序如下:
#include <stdio.h>
#include <queue>
using namespace std;
#define MAXN 21
//問題表示
int n=5;
int W=10;
int w[]={0,5,2,6,4,3};//求解結果表示
int bestw=0;
int bestx[MAXN];
int Count=1;
typedef struct
{int no;
int i;
int w;
int x[MAXN];
int ub;
} NodeType;
struct Cmp
//最多的集裝箱數
//集裝箱重量,不計下標0的元素//存放最大重量,全局變量
//存放最優解,全局變量
//搜索空間中結點數累計,全局變量//結點編號
//當前結點在解空間中的層次
//當前結點的總重量
//當前結點包含的解向量
//上界
//隊列中關系比較函數
{bool operator()(constNodeType&s,const NodeType&t)
{return (s.ub<t.ub)|| (s.ub==t.ub&& s.x[0]>t.x[0]);//ub越大越優先,當ub相同時x[0]越小越優先
}
};
voidbound(NodeType&e)
{int i=e.i+1;
int r=0;
while (i<=n)
{r+=w[i];
i++;
}
e.ub=e.w+r;
//計算分枝結點e的上界
//r為剩余集裝箱的重量51



}
voidLoading()
{NodeType e,e1,e2;
算法設計
//求裝載問題的最優解//定義3個結點
priority_queue<NodeType,vector<NodeType>,Cmp> qu;//定義一個優先隊列qu
e.no=Count++;
e.i=0;
e.w=0;
for (int j=0; j<=n;j++) e.x[j]=0;
bound(e);
qu.push(e);
while (!qu.empty())
{e=qu.top();qu.pop();
if (e.i==n)
//設置結點編號
//根結點置初值,其層次計為0//初始化根結點的解向量
//求根結點的上界
//根結點進隊
//隊不空循環
//出隊結點e作為當前結點//e是一個葉子結點
{if ((e.w>bestw)|| (e.w==bestw&& e.x[0]<bestx[0]))//比較找最優解
{bestw=e.w;
//更新bestw
for (int j=0;j<=e.i;j++)
bestx[j]=e.x[j];//復制解向量e.x->bestx
}
}
else
{if (e.w+w[e.i+1]<=W)
{e1.no=Count++;
e1.i=e.i+1;
//e不是葉子結點 //檢查左孩子結點 //設置結點編號 //建立左孩子結點
e1.w=e.w+w[e1.i];
for (int j=0; j<=e.i;j++)
e1.x[j]=e.x[j];//復制解向量e.x->e1.x
e1.x[e1.i]=1;
e1.x[0]++;
bound(e1);
qu.push(e1);
}
e2.no=Count++;
e2.i=e.i+1;
//選擇集裝箱i
//裝入集裝箱數增1//求左孩子結點的上界//左孩子結點進隊
//設置結點編號
//建立右孩子結點
e2.w=e.w;
for (int j=0; j<=e.i;j++)//復制解向量e.x->e2.xe2.x[j]=e.x[j];
e2.x[e2.i]=0;
bound(e2);
if (e2.ub>bestw)
qu.push(e2);
}
}
}
voiddisparr(intx[],intlen)
{for (int i=1;i<=len;i++)
printf("%2d",x[i]);
}
voiddispLoading()
{printf("X=[");
//不選擇集裝箱i
//求右孩子結點的上界
//若右孩子結點可行,則進隊,否則被剪枝//輸出一個解向量
//輸出最優解
52









第1章
概論
disparr(bestx,n);
printf("],裝入總價值為%d\n",bestw); }
voidmain()
{Loading();
printf("求解結果:\n");dispLoading();
}
上述程序的執行結果如圖1.40 所示。
//輸出最優解
圖1.40
程序執行結果
1.7第7 章─貪心法
1.7.1練習題
1.下面是貪心算法的基本要素的是()。
A.重疊子問題B.構造最優解C.貪心選擇性質D.定義最優解
2.下面問題()不能使用貪心法解決。
A.單源最短路徑問題B.n 皇后問題C.最小花費生成樹問題D.背包問題
3.采用貪心算法的最優裝載問題的主要計算量在於將集裝箱依其重量從小到大排序,故算法的時間復雜度為()。
A.O(n)
B.O(n)
C.O(n)
D.O(nlog2n)
4.關於0/1背包問題以下描述正確的是()。
A.可以使用貪心算法找到最優解
B.能找到多項式時間的有效算法
C.使用教材介紹的動態規划方法可求解任意0-1 背包問題
D.對於同一背包與相同的物品,做背包問題取得的總價值一定大於等於做0/1 背包問
題
5.一棵哈夫曼樹共有215個結點,對其進行哈夫曼編碼,共能得到()個不同的碼
字。
A.107
B.108
C.214
D.215
6.求解哈夫曼編碼中如何體現貪心思路?
7.舉反例證明0/1背包問題若使用的算法是按照vi/wi 的非遞減次序考慮選擇的物品,即只要正在被考慮的物品裝得進就裝入背包,則此方法不一定能得到最優解(此題說明0/1 背包問題與背包問題的不同)。
53















































































算法設計
8.求解硬幣問題。有1分、2分、5 分、10 分、50 分和100 分的硬幣各若干枚,現在要用這些硬幣來支付W 元,最少需要多少枚硬幣。
9.求解正整數的最大乘積分解問題。將正整數n分解為若干個互不相同的自然數之和,使這些自然數的乘積最大。
10.求解乘船問題。有n 個人,第i個人體重為wi(0≤i<n)。每艘船的最大載重量均為C,且最多只能乘兩個人。用最少的船裝載所有人。
11.求解會議安排問題。有一組會議A 和一組會議室B,A[i]表示第i 個會議的參加人數,B[j]表示第j 個會議室最多可以容納的人數。當且僅當A[i]≤B[j]時,第j個會議室可以用於舉辦第i 個會議。給定數組A和數組B,試問最多可以同時舉辦多少個會議。例如,A[]={1,2,3},B[]={3,2,4},結果為3;若A[]={3,4,3,1},B[]={1,2,2,6},結果為2.
12.假設要在足夠多的會場里安排一批活動,n 個活動編號為1~n,每個活動有開始
時間bi
和結束時間ei(1≤i≤n)。設計一個有效的貪心算法求出最少的會場個數。
13.給定一個m×n的數字矩陣,計算從左到右走過該矩陣且經過的方格中整數最小的路徑。一條路徑可以從第1 列的任意位置出發,到達第n 列的任意位置,每一步為從第i 列走到第i+1 列相鄰行(水平移動或沿45 度斜線移動),如圖1.41所示。第1 行和最后一行看作是相鄰的,即應當把這個矩陣看成是一個卷起來的圓筒。
圖1.41每一步的走向
兩個略有不同的5×6的數字矩陣的最小路徑如圖1.42 所示,只有最下面一行的數不同。右邊矩陣的路徑利用了第一行與最后一行相鄰的性質。
輸入:包含多個矩陣,每個矩陣的第一行為兩個數m和n,分別表示矩陣的行數和列數,接下來的m×n 個整數按行優先的順序排列,即前n 個數組成第一行,接下的n 個數組成第2 行,依此類推。相鄰整數間用一個或多個空格分隔。注意這些數不一定是正數。輸入中可能有一個或多個矩陣描述,直到輸入結束。每個矩陣的行數在1到10 之間,列數在1 到100 之間。
輸出:對每個矩陣輸出兩行,第一行為最小整數之和的路徑,路徑由n 個整數組成,表示路徑經過的行號,如果這樣的路徑不止一條,輸出字典序最小一條。
3 |
4 |
1 |
2 |
8 |
6 |
6 |
1 |
8 |
2 |
7 |
4 |
5 5 |
9 6 |
3 |
9 |
9 |
5 |
83 |
44 1 |
18 6 |
3 |
2 |
6 |
3 |
7 |
2 |
8 |
6 |
4 |
3兩 |
4數 |
1矩 |
2的 |
8小 |
6路徑 |
6 |
1 |
8 |
2 |
7 |
4 |
5 |
9 |
3 |
9 |
9 |
5 |
8 |
4 |
1 |
3 |
2 |
6 |
3 |
7 |
2 |
1 |
2 |
3 |
54






第1章
概論
6 1 8 2 7 4
5 9 3 9 9 5
8 4 1 3 2 6
3 7 2 8 6 4
輸出結果:
1 2 3 4 4 5
16
1.7.2練習題參考答案
1.答:C。
2.答:n皇后問題的解不滿足貪心選擇性質。答案為B。
3.答:D。
4.答:由於背包問題可以取物品的一部分,所以總價值一定大於等於做0/1背包問題。答案為D。
5.答:這里n=215,哈夫曼樹中n1=0,而n0=n2+1,n=n0+n1+n2=2n0-1,n0=(n+1)/2=108。答案為B。
6.答:在構造哈夫曼樹時每次都是將兩棵根結點最小的樹合並,從而體現貪心的思路。
7.證明:例如,n=3,w={3,2,2},v={7,4,4},W=4 時,由於7/3最大,若按題目要求的方法,只能取第一個,收益是7。而此實例的最大的收益應該是8,取第2、3個物品。
8.解:用結構體數組A 存放硬幣數據,A[i].v 存放硬幣i 的面額,A[i].c 存放硬幣i 的枚數。采用貪心思路,首先將數組A按面額遞減排序,再兌換硬幣,每次盡可能兌換面額大的硬幣。對應的完整程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
#define min(x,y)((x)<(y)?(x):(y))
#define MAX 21
//問題表示
int n=7;
struct NodeType
{int v;
int c;
//面額
//枚數
bool operator<(constNodeType &s)
{
//用於按面額遞減排序
return s.v<v;
}
};
NodeType A[]={{1,12},{2,8},{5,6},{50,10},{10,8},{200,1},{100,4}};
int W;
//求解結果表示
int ans=0;
voidsolve()
//兌換的硬幣枚數//兌換硬幣
55




{sort(A,A+n);
算法設計
//按面額遞減排序
for (int i=0;i<n;i++)
{int t=min(W/A[i].v,A[i].c);//使用硬幣i的枚數 if (t!=0)
printf("支付%3d面額:%3d枚\n",A[i].v,t);
W-=t*A[i].v;
ans+=t;
if (W==0) break;
}
}
voidmain()
{W=325;
//剩余的金額
//支付的金額
printf("支付%d分:\n",W);
solve();
printf("最少硬幣的個數:%d枚\n",ans);}
上述程序的執行結果如圖1.43 所示。
圖1.43
程序執行結果
9.解:采用貪心方法求解。用a[0..k]存放n 的分解結果:
(1)n≤4 時可以驗證其分解成幾個正整數的和的乘積均小於n,沒有解。
(2)n>4 時,把n分拆成若干個互不相等的自然數的和,分解數的個數越多乘積越大。為此讓n 的分解數個數盡可能多(體現貪心的思路),把n分解成從2開始的連續的自然數之和。例如,分解n 為a[0]=2,a[1]=3,a[2]=4,…,a[k]=k+2(共有k+1 個分解數),用m表示剩下數,這樣的分解直到m≤a[k]為止,即m≤k+2。對剩下數m 的處理分為如下兩種情況:
①m<k+2:將m平均分解到a[k..i](對應的分解數個數為m)中,即從a[k]開始往前的分解數增加1(也是貪心的思路,分解數越大加1 和乘積也越大)。
②m=k+2:將a[0..k-1](對應的分解數個數為k)的每個分解數增加1,剩下的2 增加到a[k]中,即a[k]增加2。
對應的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAX 20
//問題表示
int n;
//求解結果表示
56







第1章
概論
int a[MAX];
int k=0;
voidsolve()
{int i;
int sum=1;
if (n<4)
return;
else
{int m=n;
a[0]=2;
m-=a[0];
k=0;
while (m>a[k])
{k++;
//存放被分解的數
//a[0..k]存放被分解的數
//求解n的最大乘積分解問題
//不存在最優方案,直接返回
//m表示剩下數
//第一個數從2開始
//減去已經分解的數
//若剩下數大於最后一個分解數,則繼續分解 //a數組下標+1
a[k]=a[k-1]+1;//按2、3、4遞增順序分解
m-=a[k];
}
if (m<a[k])
//減去最新分解的數
//若剩下數小於a[k],從a[k]開始往前的數+1
{for (i=0; i<m;i++)
a[k-i]+=1;
}
if (m==a[k])
{a[k]+=2;
//若剩下數等於a[k],則a[k]的值+2,之前的數+1
for (i=0; i<k; i++)
a[i]+=1;
}
}
}
voidmain()
{n=23;
memset(a,0,sizeof(a));
solve();
printf("%d的最優分解方案\n",n);int mul=1;
printf("分解的數:");
for (int i=0;i<=k;i++)
if (a[i]!=0)
{printf("%d ",a[i]);
mul*=a[i];
}
printf("\n乘積最大值:%d\n",mul); }
上述程序的執行結果如圖1.44 所示。
57



圖1.44
算法設計
程序執行結果
10.解:采用貪心思路,首先按體重遞增排序;再考慮前后的兩個人(最輕者和最重者),分別用i、j 指向:若w[i]+w[j]≤C,說明這兩個人可以同乘(執行i++,j--),否則w[j]單乘(執行j--),若最后只剩余一個人,該人只能單乘。
對應的完整程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
#define MAXN 101
//問題表示
int n=7;
int w[]={50,65,58,72,78,53,82};
int C=150;
//求解結果表示
int bests=0;
voidBoat()
{sort(w,w+n);
int i=0;
int j=n - 1;
while (i<=j)
{if(i==j)
//求解乘船問題//遞增排序
//剩下最后一個人
{printf("一艘船:%d\n",w[i]); bests++;
break;
}
if (w[i]+w[j]<=C)//前后兩個人同乘
{printf("一艘船:%d%d\n",w[i],w[j]); bests++;
i++;
j--;
}
else
//w[j]單乘
{printf("一艘船:%d\n",w[j]); bests++;
j--;
}
}
}
voidmain()
{printf("求解結果:\n");
Boat();
printf("最少的船數=%d\n",bests);
}
上述程序的執行結果如圖1.45 所示。
58







第1章
概論
圖1.45
程序執行結果
11.解:采用貪心思路。每次都在還未安排的容量最大的會議室安排盡可能多的參會人數,即對於每個會議室,都安排當前還未安排的會議中,參會人數最多的會議。若能容納下,則選擇該會議,否則找參會人數次多的會議來安排,直到找到能容納下的會議。
對應的完整程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
//問題表示
int n=4;
int m=4;
int A[]={3,4,3,1};
int B[]={1,2,2,6};//求解結果表示 int ans=0;
voidsolve()
{sort(A,A+n);
sort(B,B+m);
//會議個數 //會議室個數
//求解算法//遞增排序 //遞增排序
int i=n-1,j=m-1;//從最多人數會議和最多容納人數會議室開始
for(i;i>=0;i--)
{if(A[i]<=B[j]&& j>=0)
{ans++;//不滿足條件,增加一個會議室
j--;
}
}
}
voidmain()
{solve();
printf("%d\n",ans);//輸出2
}
12.解:與《教程》例7.2類似,會場對應蓄欄,只是這里僅僅求會場個數,即最大兼容活動子集的個數。對應的完整程序如下:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAX 51
//問題表示
struct Action
//活動的類型聲明59



{int b;
int e;
算法設計
//活動起始時間 //活動結束時間
bool operator<(constAction &s) const//重載<關系函數
{if (e==s.e)
return b<=s.b;
else
return e<=s.e;
}
//結束時間相同按開始時間遞增排序//否則按結束時間遞增排序
};
int n=5;
Action A[]={{0},{1,10},{2,4},{3,6},{5,8},{4,7}};//下標0不用 //求解結果表示
int ans;
voidsolve()
{bool flag[MAX];
memset(flag,0,sizeof(flag));
sort(A+1,A+n+1);
ans=0;
for (int j=1;j<=n;j++)
{if (!flag[j])
{flag[j]=true;
int preend=j;
//最少會場個數
//求解最大兼容活動子集//活動標志
//A[1..n]按指定方式排序//會場個數
//前一個兼容活動的下標
for (int i=preend+1;i<=n;i++)
{if (A[i].b>=A[preend].e&& !flag[i]){preend=i;
flag[i]=true;
}
}
ans++;
//增加一個最大兼容活動子集
}
}
}
voidmain()
{solve();
printf("求解結果\n");
printf("最少會場個數:%d\n",ans);//輸出4
}
13.解:采用貪心思路。從第1 列開始每次查找a[i][j]元素上、中、下3 個對應數中的最小數。對應的程序如下:
#include <stdio.h>
#define M 12
#define N 110
int m=5, n=6;
int a[M][N]={{3,4,1,2,8,6},{6,1,8,2,7,4},{5,9,3,9,9,5},{8,4,1,3,2,6},{3,7,2,8,6,4}};
int minRow,minCol;
intminValue(inti,intj)
//求a[i][j]有方上、中、下3個數的最小數,同時要把行標記錄下來
{
int s = (i == 0)? m - 1 : i - 1; int x = (i == m -1)?0:i+1;
60






第1章
概論
minRow = s;
minRow = a[i][j+1]< a[minRow][j+1]?i:minRow;
minRow = a[x][j+1]< a[minRow][j+1]?x:minRow;
minRow = a[minRow][j+1]== a[s][j+1]&& minRow > s?s:minRow;minRow = a[minRow][j+1]== a[i][j+1]&& minRow > i?i:minRow;minRow = a[minRow][j+1]== a[x][j+1]&& minRow > x?x:minRow;return a[minRow][j+1];
}
voidsolve()
{int i,j,min;
for (j=n-2; j>=0;j--)
for (i=0; i<m; i++)
a[i][j]+= minValue(i,j);
min=a[0][0];
minRow=0;
for (i=1; i<m; i++)
if (a[i][0]<min)
{min=a[i][0];
minRow=i;
}
for (j=0; j<n; j++)
{printf("%d",minRow+1);
if (j<n-1) printf("");
minValue(minRow,j);
}
printf("\n%d\n",min);
}
voidmain()
{
solve();
}
//在第一列查找最小代價的行
1.8第8 章─動態規划
1.8.1練習題
1.下列算法中通常以自底向上的方式求解最優解的是()。
A.備忘錄法B.動態規划法C.貪心法D.回溯法2.備忘錄方法是()算法的變形。
A.分治法
B.回溯法
C.貪心法D.動態規划法
3.下列是動態規划算法基本要素的是()。
A.定義最優解B.構造最優解C.算出最優解D.子問題重疊性質4.一個問題可用動態規划算法或貪心算法求解的關鍵特征是問題的()。
A.貪心選擇性質B.重疊子問題C.最優子結構性質D.定義最優解5.簡述動態規划法的基本思路。
6.簡述動態規划法與貪心法的異同。
61













算法設計
7.簡述動態規划法與分治法的異同。
8.下列算法中哪些屬於動態規划算法?
(1)順序查找算法
(2)直接插入排序算法
(3)簡單選擇排序算法
(4)二路歸並排序算法
9.某個問題對應的遞歸模型如下:
f(1)=1
f(2)=2
f(n)=f(n-1)+f(n-2)+…+f(1)+1
可以采用如下遞歸算法求解:longf(intn)
{if (n==1) return1;
if (n==2) return2;
long sum=1;
for (int i=1;i<=n-1;i++)
sum+=f(i);
return sum;
當n>2 時
}
但其中存在大量的重復計算,請采用備忘錄方法求解。
10.第3 章中的實驗4采用分治法求解半數集問題,如果直接遞歸求解會存在大量重復計算,請改進該算法。
11.設計一個時間復雜度為O(n)的算法來計算二項式系數Cn(k≤n)。二項式系數Cn的求值過程如下:
𝐶=1
𝐶=1
𝑗𝑗‒1𝑗
𝑖𝑖‒1𝑖‒1
當i≥j
12.一個機器人只能向下和向右移動,每次只能移動一步,設計一個算法求它從(0,0)移動到(m,n)有多少條路徑。
13.兩種水果雜交出一種新水果,現在給新水果取名,要求這個名字中包含了以前兩種水果名字的字母,並且這個名字要盡量短。也就是說以前的一種水果名字arr1 是新水果名字arr 的子序列,另一種水果名字arr2 也是新水果名字arr 的子序列。設計一個算法求arr。
例如:輸入以下3組水果名稱:
apple peach
ananas banana
pear peach
輸出的新水果名稱如下:
62






第1章
概論
appleach
bananas
pearch
1.8.2練習題參考答案
1.答:B。
2.答:D。
3.答:D。
4.答:C。
5.答:動態規划法的基本思路是將待求解問題分解成若干個子問題,先求子問題的解,然后從這些子問題的解得到原問題的解。
6.答:動態規划法的3個基本要素是最優子結構性質、無后效性和重疊子問題性質,而貪心法的兩個基本要素是貪心選擇性質和最優子結構性質。所以兩者的共同點是都要求問題具有最優子結構性質。
兩者的不同點如下:
(1)求解方式不同,動態規划法是自底向上的,有些具有最優子結構性質的問題只能用動態規划法,有些可用貪心法。而貪心法是自頂向下的。
(2)對子問題的依賴不同,動態規划法依賴於各子問題的解,所以應使各子問題最優,才能保證整體最優;而貪心法依賴於過去所作過的選擇,但決不依賴於將來的選擇,也不依賴於子問題的解。
7.答:兩者的共同點是將待求解的問題分解成若干子問題,先求解子問題,然后再從這些子問題的解得到原問題的解。
兩者的不同點是:適合於用動態規划法求解的問題,分解得到的各子問題往往不是相互獨立的(重疊子問題性質),而分治法中子問題相互獨立;另外動態規划法用表保存已求解過的子問題的解,再次碰到同樣的子問題時不必重新求解,而只需查詢答案,故可獲得多項式級時間復雜度,效率較高,而分治法中對於每次出現的子問題均求解,導致同樣的子問題被反復求解,故產生指數增長的時間復雜度,效率較低。
8.答:判斷算法是否具有最優子結構性質、無后效性和重疊子問題性質。(2)、(3)和(4)均屬於動態規划算法。
9.解:設計一個dp 數組,dp[i]對應f(i)的值,首先dp 的所有元素初始化為0,在計算f(i)時,若dp[0]>0表示f(i)已經求出,直接返回dp[i]即可,這樣避免了重復計算。對應的算法如下:
long dp[MAX];//dp[n]保存f(n)的計算結果
longf1(intn)
{if (n==1)
{dp[n]=1;
return dp[n];
}
if (n==2)
{dp[n]=2;
return dp[n];
63





算法設計
}
if (dp[n]>0) returndp[n];
long sum=1;
for (int i=1;i<=n-1;i++)
sum+=f1(i);
dp[n]=sum;
return dp[n];
}
10.解:設計一個數組a,其中a[i]=f(i),首先將a的所有元素初始化為0,當a[i]>0時表示對應的f(i)已經求出,直接返回就可以了。對應的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAXN 201
//問題表示
int n;
int a[MAXN];
intfa(inti)
{int ans=1;
if (a[i]>0)
return a[i];
for(int j=1;j<=i/2;j++)
ans+=fa(j);
a[i]=ans;
return ans;
}
intsolve(int n)
{memset(a,0,sizeof(a));
a[1]=1;
return fa(n);
}
voidmain()
{n=6;
//求a[i]
//求set(n)的元素個數
printf("求解結果\n");
printf("n=%d時半數集元素個數=%d\n",n,solve(n));
}
11.解:定義C(i,j)=Ci
,i≥j。則有如下遞推計算公式:C(i,nj)=C(i-1,j-1)+C(i-
1,j),初始條件為C(i,0)=1,C(i,i)=1。可以根據初始條件由此遞推關系計算C(n,k),
n
即Cn。對應的程序如下:
#include <stdio.h>
#define MAXN 51
#define MAXK 31
//問題表示
int n,k;
//求解結果表示
int C[MAXN][MAXK];
voidsolve()
{int i,j;
64







第1章
概論
for (i=0;i<=n;i++)
{C[i][i]=1;
C[i][0]=1;
}
for (i=1;i<=n;i++)
for (j=1;j<=k;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
voidmain()
{n=5,k=3;
solve();
printf("%d\n",C[n][k]);//輸出10
}
顯然,solve()算法的時間復雜度為O(n)。
12.解:設從(0,0)移動到(i,j)的路徑條數為dp[i][j],由於機器人只能向下和向右移動,不同於迷宮問題(迷宮問題由於存在后退,不滿足無后效性,不適合用動態規划法求解)。對應的狀態轉移方程如下:
dp[0][j]=1
dp[i][0]=1
dp[i][j]=dp[i][j-1]+dp[i-1][j]
i、j>0
最后結果是dp[m][n]。對應的程序如下:
#include <stdio.h>
#include <string.h>
#define MAXX 51
#define MAXY 51
//問題表示
int m,n;
//求解結果表示
int dp[MAXX][MAXY];
voidsolve()
{int i,j;
dp[0][0]=0;
memset(dp,0,sizeof(dp));
for (i=1;i<=m;i++)
dp[i][0]=1;
for (j=1;j<=n;j++)
dp[0][j]=1;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
dp[i][j]=dp[i][j-1]+dp[i-1][j];
}
voidmain()
{m=5,n=3;
solve();
printf("%d\n",dp[m][n]);
}
13.解:本題目的思路是求arr1 和arr2字符串的最長公共子序列,基本過程參見《教
65



程》第8 章8.5 節。對應的完整程序如下:#include <iostream>
#include <string.h>
#include <vector>
#include <string>
using namespace std;
#define max(x,y)((x)>(y)?(x):(y))
#define MAX 51
//問題表示
int m,n;
string arr1,arr2;
//求解結果表示
int dp[MAX][MAX];
vector<char> subs;
voidLCSlength()
{int i,j;
for (i=0;i<=m;i++)
dp[i][0]=0;
for (j=0;j<=n;j++)
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
算法設計
//序列中最多的字符個數
//動態規划數組
//存放LCS
//求dp
//將dp[i][0]置為0,邊界條件
//將dp[0][j]置為0,邊界條件
//兩重for循環處理arr1、arr2的所有字符
{if (arr1[i-1]==arr2[j-1])//比較的字符相同
dp[i][j]=dp[i-1][j-1]+1;
else
//比較的字符不同
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
}
voidBuildsubs()
{int k=dp[m][n];
int i=m;
int j=n;
while (k>0)
//由dp構造從subs
//k為arr1和arr2的最長公共子序列長度
//在subs中放入最長公共子序列(反向)
if (dp[i][j]==dp[i-1][j])i--;
else if (dp[i][j]==dp[i][j-1])j--;
else
{subs.push_back(arr1[i-1]);//subs中添加arr1[i-1] i--; j--; k--;
}
}
voidmain()
{cin >> arr1>> arr2; m=arr1.length();
n=arr2.length();
LCSlength();
Buildsubs();
cout << "求解結果"<<endl;
cout << "arr:";vector<char>::reverse_iteratorrit;
//輸入arr1和arr2 //m為arr1的長度 //n為arr2的長度 //求出dp
//求出LCS
66






第1章
for (rit=subs.rbegin();rit!=subs.rend();++rit)
cout << *rit;
cout << endl;
cout << "長度:"<<dp[m][n]<< endl;
概論
}
改為如下:
13.解:本題目的思路是先求arr1 和arr2 字符串的最長公共子序列,基本過程參見《教程》第8 章8.5節,再利用遞歸輸出新水果取名。
算法中設置二維動態規划數組dp,dp[i][j]表示arr1[0..i-1](i 個字母)和arr2[0..j-1](j 個字母)中最長公共子序列的長度。另外設置二維數組b,b[i][j]表示arr1 和arr2 比較的3 種情況:b[i][j]=0表示arr1[i-1]=arr2[j-1],b[i][j]=1表示arr1[i-1]≠arr2[j-1]並且dp[i- 1][j]>dp[i][j-1],b[i][j]=2表示arr1[i-1]≠arr2[j-1]並且dp[i-1][j]≤dp[i][j-1]。
對應的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAX 51
//問題表示
int m,n;
char arr1[MAX],arr2[MAX];
//求解結果表示
int dp[MAX][MAX];
int b[MAX][MAX];
void Output(int i,intj)
{if (i==0 &&j==0)
return;
if(i==0)
{Output(i,j-1);
printf("%c",arr2[j-1]);
return;
}
else if(j==0)
{Output(i-1,j);
printf("%c",arr1[i-1]);
return;
}
if (b[i][j]==0)
{Output(i-1,j-1);
printf("%c",arr1[i-1]);
return;
}
else if(b[i][j]==1)
{Output(i-1,j);
printf("%c",arr1[i-1]);
return;
}
else
{Output(i,j-1);
//序列中最多的字符個數
//動態規划數組
//存放arr1與arr2比較的3種情況//利用遞歸輸出新水果取名
//輸出完畢
//arr1完畢,輸出arr2的剩余部分
//arr2完畢,輸出arr1的剩余部分
//arr1[i-1]=arr2[j-1]的情況
67



算法設計
printf("%c",arr2[j-1]);
return;
}
}
void LCSlength()
{int i,j;
for (i=0;i<=m;i++)
dp[i][0]=0;
for (j=0;j<=n;j++)
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
{if (arr1[i-1]==arr2[j-1])
{dp[i][j]=dp[i-1][j-1]+1;
b[i][j]=0;
}
else if (dp[i-1][j]>dp[i][j-1])
{dp[i][j]=dp[i-1][j];
b[i][j]=1;
}
else
{dp[i][j]=dp[i][j-1];
b[i][j]=2;
}
}
}
void main()
{int t;
printf("測試用例個數:");scanf("%d",&t);
while(t--)
{scanf("%s",arr1);
scanf("%s",arr2);
memset(b,-1,sizeof(b));
m=strlen(arr1);
n=strlen(arr2);
LCSlength();
printf("結果: ");Output(m,n); printf("\n");
}
}
上述程序的一次執行結果如圖1.46 所示。
//求dp
//將dp[i][0]置為0,邊界條件
//將dp[0][j]置為0,邊界條件
//兩重for循環處理arr1、arr2的所有字符 //比較的字符相同:情況0
//情況1
//dp[i-1][j]<=dp[i][j-1]:情況2
//輸入測試用例個數
//m為arr1的長度
//n為arr2的長度
//求出dp
//輸出新水果取名
68







第1章
概論
圖1.46
程序的一次執行結果
13.解:本題目的思路是求arr1 和arr2字符串的最長公共子序列,基本過程參見《教程》第8 章8.5 節。對應的完整程序如下:
然后再用遞歸思想,逐一輸出,得到的就是最后答案。
#include <iostream>
#include <string.h>
#include <vector>
#include <string>
using namespace std;
#define max(x,y)((x)>(y)?(x):(y))
#define MAX 51
//問題表示
int m,n;
string arr1,arr2;
//求解結果表示
int dp[MAX][MAX];
vector<char> subs;
voidLCSlength()
{int i,j;
for (i=0;i<=m;i++)
dp[i][0]=0;
for (j=0;j<=n;j++)
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
//序列中最多的字符個數
//動態規划數組
//存放LCS
//求dp
//將dp[i][0]置為0,邊界條件
//將dp[0][j]置為0,邊界條件
//兩重for循環處理arr1、arr2的所有字符
{if (arr1[i-1]==arr2[j-1])//比較的字符相同
dp[i][j]=dp[i-1][j-1]+1;
else
//比較的字符不同
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
}
voidBuildsubs()
{int k=dp[m][n];
int i=m;
int j=n;
while (k>0)
//由dp構造從subs
//k為arr1和arr2的最長公共子序列長度
//在subs中放入最長公共子序列(反向)
if (dp[i][j]==dp[i-1][j])i--;
else if (dp[i][j]==dp[i][j-1])j--;
69




算法設計
else
{subs.push_back(arr1[i-1]);//subs中添加arr1[i-1] i--; j--; k--;
}
}
voidmain()
{cin >> arr1>> arr2; m=arr1.length();
n=arr2.length();
LCSlength();
Buildsubs();
//輸入arr1和arr2 //m為arr1的長度 //n為arr2的長度 //求出dp
//求出LCS
cout << "求解結果"<<endl;
cout << "arr:";
vector<char>::reverse_iteratorrit;
for (rit=subs.rbegin();rit!=subs.rend();++rit)
cout << *rit;
cout << endl;
cout << "長度:"<<dp[m][n]<< endl;
}
上述程序的一次執行結果如圖1.46所示。
圖1.46
程序的一次執行結果
1.9第9 章─圖算法設計
1.9.1練習題
1.以下不屬於貪心算法的是()。
A.Prim 算法B.Kruskal 算法C.Dijkstra算法D.深度優先遍歷2.一個有n 個頂點的連通圖的生成樹是原圖的最小連通子圖,且包含原圖中所有n
個頂點,並且有保持圖聯通的最少的邊。最大生成樹就是權和最大生成樹,現在給出一個無向帶權圖的鄰接矩陣為{{0,4,5,0,3},{4,0,4,2,3},{5,4,0,2,0},{0,2,2,0,1},{3,3,0,1,0}},其中權為0 表示沒有邊。一個圖為求這個圖的最大生成樹的權和是()。
A.11
B.12
C.13
D.14
E.15
3.某個帶權連通圖有4個以上的頂點,其中恰好有2 條權值最小的邊,盡管該圖的最小生成樹可能有多個,而這2 條權值最小的邊一定包含在所有的最小生成樹中嗎?如果有3 條權值最小的邊呢?
70
















第1章
概論
4.為什么TSP 問題采用貪心算法求解不一定得到最優解?
5.求最短路徑的4 種算法適合帶權無向圖嗎?
6.求單源最短路徑的算法有Dijkstra 算法、Bellman-Ford 算法和SPFA 算法,比較這些算法的不同點。
7.有人這樣修改Dijkstra 算法以便求一個帶權連通圖的單源最長路徑,將每次選擇dist 最小的頂點u改為選擇最大的頂點u,將按路徑長度小進行調整改為按路徑長度大調整。這樣可以求單源最長路徑嗎?
8.給出一種方法求無環帶權連通圖(所有權值非負)中從頂點s 到頂點t的一條最長簡單路徑。
9.一個運輸網絡如圖1.47 所示,邊上數字為(c(i,j),b(i,j)),其中c(i,j)表示容量,b(i,j)表示單位運輸費用。給出從1、2、3 位置運輸貨物到位置6的最小費用最大流的過程。
10.本教程中的Dijkstra 算法采用鄰接矩陣存儲圖,算法時間復雜度為O(n)。請你從各個方面考慮優化該算法,用於求源點v 到其他頂點的最短路徑長度。
11.有一個帶權有向圖G(所有權為正整數),采用鄰接矩陣存儲。設計一個算法求其中的一個最小環。
1
(4,2)
(6,4)
4
(12,3)
2
(3,5)
6
(3,4)
(1,6)
5
(9,12)
3
(2,3)
圖1.47
一個運輸網絡
1.9.2練習題參考答案
1.答:D。
2.答:采用類似Kurskal 算法來求最大生成樹,第1 步取最大邊(0,2),第2步取邊(0,1),第3步取邊(0,4),第4步取最大邊(1,3),得到的權和為14。答案為D。
3.答:這2 條權值最小的邊一定包含在所有的最小生成樹中,因為按Kurskal算法一定首先選中這2 條權值最小的邊。如果有3 條權值最小的邊,就不一定了,因為首先選中這3 條權值最小的邊有可能出現回路。
4.答:TSP 問題不滿足最優子結構性質,如(0,1,2,3,0)是整個問題的最優解,但(0,1,2,0)不一定是子問題的最優解。
5.答:都適合帶權無向圖求最短路徑。
6.答:Dijkstra 算法不適合存在負權邊的圖求單源最短路徑,其時間復雜度為O(n)。Bellman-Ford 算法和SPFA 算法適合存在負權邊的圖求單源最短路徑,但圖中不能
71
















算法設計
存在權值和為負的環。Bellman-Ford 算法的時間復雜度為O(ne),而SPFA算法的時間復雜度為O(e),所以SPFA 算法更優。
7.答:不能。Dijkstra算法本質上是一種貪心算法,而求單源最長路徑不滿足貪心選擇性質。
8.答:Bellman-Ford 算法和SPFA算法適合存在負權邊的圖求單源最短路徑。可以將圖中所有邊權值改為負權值,求出從頂點s 到頂點t的一條最短簡單路徑,它就是原來圖中從頂點s 到頂點t的一條最長簡單路徑。
9.答:為該運輸網絡添加一個虛擬起點0,它到1、2、3 位置運輸費用為0,容量分別為到1、2、3 位置運輸容量和,如圖1.48 所示,起點s=0,終點t=6。
1
(4,2)
(10,0)
(6,4)
4
(12,3)
0
(6,0)
2
(3,5)
6
(3,0)
(3,4)
(1,6)
5
(9,12)
3
(2,3)
圖1.48
添加一個虛擬起點的運輸網絡
首先初始化f 為零流,最大流量maxf=0,最小費用mincost=0,采用最小費用最大流算法求解過程如下:
(1)k=0,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
∞ |
0 |
∞ |
∞ |
2 |
4 |
∞ |
∞ |
∞ |
0 |
∞ |
5 |
4 |
∞ |
∞ |
∞ |
∞ |
0 |
6 |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
求出從起點0 到終點6 的最短路徑為0→1→4→6,求出最小調整量=4,f[4][6]調整為4,f[1][4]調整為4,f[0][1]調整為4,mincost=20,maxf=4。
(2)k=1,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
∞ |
∞ |
0 |
∞ |
5 |
4 |
∞ |
∞ |
∞ |
∞ |
0 |
6 |
3 |
∞ |
∞ |
-2 |
∞ |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
求出從起點0 到終點6 的最短路徑為0→2→4→6,求出最小調整量=3,f[4][6]調整
72






第1章
概論
為7,f[2][4]調整為3,f[0][2]調整為3,mincost=44,maxf=4+3=7。(3)k=2,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
∞ |
∞ |
∞ |
0 |
6 |
3 |
∞ |
∞ |
-2 |
-5 |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
求出從起點0 到終點6 的最短路徑為0→3→4→6,求出最小調整量=1,f[4][6]調整為8,f[3][4]調整為1,f[0][3]調整為1,mincost=53,maxf=7+1=8。
(4)k=3,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
-2 |
-5 |
-6 |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
求出從起點0 到終點6 的最短路徑為0→3→5→6,求出最小調整量=2,f[5][6]調整為2,f[3][5]調整為2,f[0][3]調整為3,mincost=83,maxf=8+2=10。
(5)k=4,求出w如下:
0 |
0 |
0 |
∞ |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
∞ |
0 |
∞ |
∞ |
∞ |
∞ |
-2 |
-5 |
-6 |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
-12 |
0 |
求出從起點0 到終點6 的最短路徑為0→1→5→6,求出最小調整量=6,f[5][6]調整為8,f[1][5]調整為6,f[0][1]調整為10,mincost=179,maxf=10+6=16。
(6)k=5,求出w如下:
0 |
∞ |
0 |
∞ |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
∞ |
0 |
∞ |
∞ |
∞ |
∞ |
-2 |
-5 |
-6 |
0 |
∞ |
3 |
∞ |
-4 |
∞ |
-3 |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
-12 |
0 |
求出從起點0 到終點6 的最短路徑為0→1→5→6,求出最小調整量=1,f[5][6]調整
73



算法設計
為9,f[2][5]調整為1,f[0][2]調整為4,mincost=195,maxf=16+1=17。(7)k=6,求出的w中沒有增廣路徑,調整結束。對應的最大流如下:
0 |
10 |
4 |
3 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
4 |
6 |
0 |
0 |
0 |
0 |
0 |
3 |
1 |
0 |
0 |
0 |
0 |
0 |
1 |
2 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
8 |
0 |
0 |
0 |
0 |
0 |
0 |
9 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
最終結果,maxf=17,mincost=195。即運輸的最大貨物量為17,對應的最小總運輸費用為195。
10.解:從兩個方面考慮優化:
(1)在Dijkstra 算法中,當求出源點v 到頂點u 的最短路徑長度后,僅僅調整從頂點u 出發的鄰接點的最短路徑長度,而教程中的Dijkstra算法由於采用鄰接矩陣存儲圖,需要花費O(n)的時間來調整頂點u 出發的鄰接點的最短路徑長度,如果采用鄰接表存儲圖,可以很快查找到頂點u 的所有鄰接點並進行調整,時間為O(MAX(圖中頂點的出度))。
(2)求目前一個最短路徑長度的頂點u 時,教科書上的Dijkstra 算法采用簡單比較方法,可以改為采用優先隊列(小根堆)求解。由於最多e 條邊對應的頂點進隊,對應的時間為O(log2e)。
對應的完整程序和測試數據算法如下:
#include "Graph.cpp"
#include <queue>
#include <string.h>
using namespace std;
ALGraph *G;
struct Node
{int i;
int v;
//包含圖的基本運算算法
//圖的鄰接表存儲結構,作為全局變量 //聲明堆中結點類型
//頂點編號
//dist[i]值
friend bool operator<(constNode &a,const Node &b)//定義比較運算符 {returna.v >b.v;}
};
voidDijkstra(intv,intdist[])//改進的Dijkstra算法
{ArcNode *p;
priority_queue<Node>qu;
Node e;
int S[MAXV];
//創建小根堆
//S[i]=1表示頂點i在S中,S[i]=0表示頂點i在U中
int i,j,u,w;
memset(S,0,sizeof(S));
p=G->adjlist[v].firstarc;
for (i=0;i<G->n;i++)dist[i]=INF;
while (p!=NULL)
{w=p->adjvex;
74






第1章
概論
dist[w]=p->weight;
e.i=w; e.v=dist[w];
qu.push(e);
p=p->nextarc;
}
S[v]=1;
for (i=0;i<G->n-1;i++)
//距離初始化
//將v的出邊頂點進隊qu
//源點編號v放入S中
//循環直到所有頂點的最短路徑都求出
{e=qu.top();qu.pop();//出隊e
u=e.i;
S[u]=1;
p=G->adjlist[u].firstarc;
while (p!=NULL)
{w=p->adjvex;
if (S[w]==0)
//選取具有最小最短路徑長度的頂點u
//頂點u加入S中
//考察從頂點u出發的所有相鄰點
//考慮修改不在S中的頂點w的最短路徑長度
if (dist[u]+p->weight<dist[w])
{dist[w]=dist[u]+p->weight;//修改最短路徑長度 e.i=w; e.v=dist[w];
qu.push(e);//修改最短路徑長度的頂點進隊
}
p=p->nextarc;
}
}
}
voidDisppathlength(intv,intdist[])//輸出最短路徑長度
{printf("從%d頂點出發的最短路徑長度如下:\n",v);
for (int i=0;i<G->n;++i)
if (i!=v)
printf("到頂點%d:%d\n",i,dist[i]);
}
voidmain()
{int A[MAXV][MAXV]={
{0,4,6,6,INF,INF,INF},
{INF,0,1,INF,7,INF,INF},
{INF,INF,0,INF,6,4,INF},
{INF,INF,2,0,INF,5,INF},
{INF,INF,INF,INF,0,INF,6},
{INF,INF,INF,INF,1,0,8},
{INF,INF,INF,INF,INF,INF,0}};
int n=7, e=12;
CreateAdj(G,A,n,e);printf("圖G的鄰接表:\n");DispAdj(G);
int v=0;
int dist[MAXV];
Dijkstra(v,dist);
Disppathlength(v,dist);
DestroyAdj(G);
//建立圖的鄰接表 //輸出鄰接表
//調用Dijkstra算法//輸出結果
//銷毀圖的鄰接表
}
上述程序的執行結果如圖1.49 所示。
75




算法設計
圖1.49程序執行結果
其中Dijkstra 算法的時間復雜度為O(n(log2e+MAX(頂點的出度)),一般圖中最大頂點出度遠小於e,所以進一步簡化時間復雜度為O(nlog2e)。
11.有一個帶權有向圖G(所有權為正整數),采用鄰接矩陣存儲。設計一個算法求其中的一個最小環。
解:利用Floyd 算法求出所有頂點對之間的最短路徑,若頂點i 到j 有最短路徑,而圖中又存在頂點j到i的邊,則構成一個環,在所有環中比較找到一個最小環並輸出。對應的程序如下:
#include "Graph.cpp"
//包含圖的基本運算算法
#include <vector>
using namespace std;
voidDispapath(intpath[][MAXV],inti,intj) //輸出頂點i到j的一條最短路徑
{vector<int>apath;
int k=path[i][j];
apath.push_back(j);
while (k!=-1 && k!=i)
{apath.push_back(k);
k=path[i][k];
}
apath.push_back(i);
//存放一條最短路徑中間頂點(反向)//路徑上添加終點
//路徑上添加中間點
//路徑上添加起點
for (int s=apath.size()-1;s>=0;s--)//輸出路徑上的中間頂點printf("%d→",apath[s]);
}
intMincycle(MGraphg,intA[MAXV][MAXV],int&mini,int&minj)//在圖g和A中的查找一個最小環
{int i,j,min=INF;
for (i=0;i<g.n;i++)
for (j=0;j<g.n;j++)
if (i!=j && g.edges[j][i]<INF)
{if (A[i][j]+g.edges[j][i]<min)
{min=A[i][j]+g.edges[j][i];
mini=i; minj=j;
76






第1章
概論
}
}
return min;
}
voidFloyd(MGraphg)
{int A[MAXV][MAXV],path[MAXV][MAXV];
int i,j,k,min,mini,minj;
for (i=0;i<g.n;i++)
for (j=0;j<g.n;j++)
{A[i][j]=g.edges[i][j];
//Floyd算法求圖g中的一個最小環
if (i!=j && g.edges[i][j]<INF)
path[i][j]=i;
else
path[i][j]=-1;
}
for (k=0;k<g.n;k++)
{for (i=0;i<g.n;i++)
for (j=0;j<g.n;j++)
//頂點i到j有邊時
//頂點i到j沒有邊時
//依次考察所有頂點
if (A[i][j]>A[i][k]+A[k][j])
{A[i][j]=A[i][k]+A[k][j];//修改最短路徑長度 path[i][j]=path[k][j];//修改最短路徑
}
}
min=Mincycle(g,A,mini,minj);
if (min!=INF)
{printf("圖中最小環:");
Dispapath(path,mini,minj);printf("%d, 長度:%d\n",mini,min);
}
else printf("圖中沒有任何環\n");
}
voidmain()
{MGraph g;
//輸出一條最短路徑
int A[MAXV][MAXV]={{0,5,INF,INF},{INF,0,1,INF},
{3,INF,0,2}, {INF,4,INF,0}};
int n=4, e=5;
CreateMat(g,A,n,e);printf("圖G的鄰接矩陣:\n");DispMat(g);
Floyd(g);
//建立圖的鄰接矩陣//輸出鄰接矩陣
}
上述程序的執行結果如圖1.50 所示。
77





算法設計
圖1.50程序執行結果
1.10第10章─計算幾何
1.10.1練習題
1.對如圖1.51 所示的點集A,給出采用Graham 掃描算法求凸包的過程及結果。
10
9
8
7
6
5
4
3
2
1
a8
a11
a9
a0
a6
a7
a5
a3
a10
a4
a1
a2
12345
678910
圖1.51一個點集A
2.對如圖1.51 所示的點集A,給出采用分治法求最近點對的過程及結果。
3.對如圖1.51 所示的點集A,給出采用旋轉卡殼法求最遠點對的結果。
4.對應3 個點向量p1、p2、p3,采用S(p1,p2,p3)=(p2-p1)(p3-p1)/2求它們構成的三角形面積,請問什么情況下計算結果為正?什么情況下計算結果為負?
5.已知坐標為整數,給出判斷平面上一點p 是否在一個逆時針三角形p1-p2-p3的算法。
1.10.2練習題參考答案
內部
1.答:采用Graham 掃描算法求凸包的過程及結果如下:
求出起點a0(1,1)。
排序后:a0(1,1)a1(8,1)a2(9,4)a3(5,4)a4(8,7)a5(5,6)a10(7,10)a9(3,5) a6(3,7) a7(4,10)a8(1,6)a11(0,3)。
先將a0(1,1)進棧,a1(8,1)進棧,a2(9,4)進棧。
處理點a3(5,4):a3(5,4)進棧。
處理點a4(8,7):a3(5,4)存在右拐關系,退棧,a4(8,7)進棧。
78






第1章
概論
處理點a5(5,6):a5(5,6)進棧。
處理點a10(7,10):a5(5,6)存在右拐關系,退棧,a10(7,10)進棧。
處理點a9(3,5):a9(3,5)進棧。
處理點a6(3,7):a9(3,5)存在右拐關系,退棧,a6(3,7)進棧。
處理點a7(4,10):a6(3,7)存在右拐關系,退棧,a7(4,10)進棧。
處理點a8(1,6):a8(1,6)進棧。
處理點a11(0,3):a11(0,3)進棧。
結果:n=8,凸包的頂點:a0(1,1)a1(8,1)a2(9,4)a4(8,7)a10(7,10)a7(4,10) a8(1,6) a11(0,3)。
2.答:求解過程如下:
排序前:(1,1)(8,1)(9,4)(5,4)(8,7)(5,6)(3,7)(4,10)(1,6)(3,5)(7,10)(0,3)。按x 坐標排序后:(0,3)(1,1)(1,6)(3,7)(3,5)(4,10)(5,4)(5,6)(7,10) (8,1)(8,7)(9,4)。按y 坐標排序后:(1,1)(8,1)(0,3)(5,4)(9,4)(3,5)(1,6) (5,6) (3,7) (8,7) (4,10) (7,10)。
(1)中間位置midindex=5,左部分:(0,3)(1,1)(1,6)(3,7)(3,5)(4,10);右部分:(5,4)(5,6)(7,10)(8,1)(8,7)(9,4);中間部分點集為(0,3)(3,7)(4,10)(5,4) (5,6) (7,10) (8,7)。
(2)求解左部分:(0,3) (1,1) (1,6) (3,7) (3,5)(4,10)。
中間位置=2,划分為左部分1:(0,3)(1,1)(1,6),右部分1:(3,7) (3,5) (4,10)處理左部分1:點數少於4:求出最近距離=2.23607,即(0,3)和(1,1)之間的距離。處理右部分1:點數少於4:求出最近距離=2,即(3,7)和(3,5)之間的距離。再考慮中間部分(中間部分最近距離=2.23)求出左部分d1=2。
(3)求解右部分:(5,4) (5,6) (7,10) (8,1) (8,7)(9,4)。
中間位置=8,划分為左部分2:(5,4)(5,6)(7,10),右部分2:(8,1)(8,7)(9,4)。
處理左部分2:點數少於4,求出最近距離=2,即(5,4)和(5,6)之間的距離。處理右部分2:點數少於4,求出最近距離=3.16228,即(8,1)和(9,4)之間的距離。再考慮中間部分(中間部分為空)求出右部分d2=2。
(4)求解中間部分點集:(0,3)(3,7)(4,10)(5,4)(5,6)(7,10)(8,7)。求出最近距離d3=5。
最終結果為:d=MIN{d1,d2,d3)=2。
3.答:采用旋轉卡殼法求出兩個最遠點對是(1,1)和(7,10),最遠距離為10.82。
4.答:當三角形p1-p2-p3
逆時針方向時,如圖1.52 所示,p2-p1
在p3-p1
的順時針方
向上,(p2-p1)(p3-p1)>0,對應的面積(p2-p1)(p3-p1)/2為正。
當三角形p1-p2-p3
順時針方向時,如圖1.53 所示,p2-p1
在p3-p1
的逆時針方向上,
(p2-p1)(p3-p1)<0,對應的面積(p2-p1)(p3-p1)/2為負。
79























算法設計
p1
p3-p1
p2-p1
p1
p2-p1
p3-p1
圖1.52
p1-p2-p3
逆時針方向圖
圖1.53p1-p2-p3
逆時針方向
5.答:用S(p1,p2,p3)=(p2-p1)(p3-p1)/2求三角形p1、p2、p3 帶符號的的面積。如圖1.54 所示,若S(p,p2,p3)和S(p,p3,p1)和S(p,p1,p2)(3 個三角形的方向均為逆時針方向)均大於0,表示p 在該三角形內部。
p3
p1
p
p2
圖1.54
一個點p 和一個三角形
對應的程序如下:
#include "Fundament.cpp"
//包含向量基本運算算法
doublegetArea(Pointp1,Pointp2,Pointp3)//求帶符號的面積
{
return Det(p2-p1,p3-p1);
}
boolIntrig(Pointp,Pointp1,Pointp2,Pointp3)//判斷p是否在三角形p1p2p3的內部{double area1=getArea(p,p2,p3);
double area2=getArea(p,p3,p1);
double area3=getArea(p,p1,p2);
if (area1>0 && area2>0&& area3>0)
return true;
else
return false;
}
voidmain()
{printf("求解結果\n");
Point p1(0,0);
Point p2(5,-4);
Point p3(4,3);
Point p4(3,1);
Point p5(-1,1);
printf("p1:");p1.disp(); printf("\n");
printf("p2:");p2.disp(); printf("\n");
printf("p3:");p3.disp(); printf("\n");
printf("p4:");p4.disp(); printf("\n");
80







第1章
概論
printf("
printf("
printf("
printf("
p5:"); p5.disp();printf("\n");
p1p2p3三角形面積:%g\n",getArea(p1,p2,p3));
p4在p1p2p3三角形內部:%s\n",Intrig(p4,p1,p2,p3)?"是":"不是");p5在p1p2p3三角形內部:%s\n",Intrig(p5,p1,p2,p3)?"是":"不是");
}
上述程序的執行結果如圖1.55 所示。
圖1.55程序執行結果
1.11第11章─計算復雜性理論
1.11.1練習題
1.旅行商問題是NP 問題嗎?
A.否
B.是
C.至今尚無定論
2.下面有關P 問題,NP問題和NPC 問題,說法錯誤的是()。
A.如果一個問題可以找到一個能在多項式的時間里解決它的算法,那么這個問題就屬於P 問題
B.NP 問題是指可以在多項式的時間里驗證一個解的問題
C.所有的P 類問題都是NP問題
D.NPC 問題不一定是個NP 問題,只要保證所有的NP問題都可以約化到它即可
3.對於《教程》例11.2 設計的圖靈機,分別給出執行f(3,2)和f(2,3)的瞬像演變過程。
4.什么是P 類問題?什么是NP類問題?
5.證明求兩個m 行n 列的二維矩陣相加的問題屬於P 類問題。
6.證明求含有n 個元素的數據序列中求最大元素的問題屬於P 類問題。
7.設計一個確定性圖靈機M,用於計算后繼函數S(n)=n+1(n為一個二進制數),並給出求1010001 的后繼函數值的瞬像演變過程。
1.11.2練習題參考答案
1.答:B。
2.答:D。
3.答:(1)執行f(3,2)時,輸入帶上的初始信息為000100B,其瞬像演變過程如
81



算法設計
下:
q0000100BB0q30110BBB01q210B
Bq100100BBq300110BBB011q20B
B0q10100Bq3B00110BBB01q311B
B00q1100B
B001q200B
B00q3110B
Bq000110B
BBq10110B
BB0q1110B
BB0q3111B
BBq30111B
BBq00111B
BBB1q211B
BBB11q21B
BBB111q2B
BBB11q41B
BBB1q41BB
BBBq41BBB
BBBq4BBBB
BBB0q6BBB
最終帶上有一個0,計算結果為1。
(2)執行f(2,3)時,輸入帶上的初始信息為001000B,其瞬像演變過程如下:
q0001000B
Bq001000B
B0q11000B
B01q2000B
B0q31100B
Bq301100B
q3B01100BBBq31100B
Bq001100BBq3B1100B
BBq11100BBBq01100B
BB1q2100BBBBq5100B
BB11q200BBBBBq500B
BB1q3100B
BBBBBq50B
BBBBBBq5B
BBBBBBBq6
最終帶上有零個0,計算結果為0。
4.答:用確定性圖靈機以多項式時間界可解的問題稱為P 類問題。用非確定性圖靈機以多項式時間界可解的問題稱為NP類問題。
5.答:求兩個m 行n列的二維矩陣相加的問題對應的算法時間復雜度為O(mn),所以屬於P 類問題。
6.答:求含有n個元素的數據序列中最大元素的問題的算法時間復雜度為O(n),所以屬於P 類問題。
7.解:q0 為初始狀態,q3為終止狀態,讀寫頭初始時注視最右邊的格。δ 動作函數如下:
δ(q0,0)→(q1,1,L)
δ(q0,1)→(q2,0,L)
δ(q0,B)→(q3,B,R)
δ(q1,0)→(q1,0,L)
δ(q1,1)→(q1,1,L)
δ(q1,B)→(q3,B,L)
δ(q2,0)→(q1,1,L)
δ(q2,1)→(q2,0,L)
δ(q2,B)→(q3,B,L)
求10100010 的后繼函數值的瞬像演變過程如下:
B1010001q00B
B101000q111B
B10100q1011B
B1010q10011B
B101q100011B
B10q1100011B
B1q10100011B
Bq110100011B
q1B10100011B
q3BB10100011B
其結果為10100011。
82






第1章
概論
1.12第12章─概率算法和近似算法
1.12.1練習題
1.蒙特卡羅算法是()的一種。
A.分枝限界算法B.貪心算法C.概率算法D.回溯算法
2.在下列算法中有時找不到問題解的是()。
A.蒙特卡羅算法B.拉斯維加斯算法C.舍伍德算法D.數值概率算法3.在下列算法中得到的解未必正確的是()。
A.蒙特卡羅算法B.拉斯維加斯算法C.舍伍德算法D.數值概率算法4.總能求得非數值問題的一個解,且所求得的解總是正確的是()。
A.蒙特卡羅算法B.拉斯維加斯算法C.數值概率算法D.舍伍德算法5.目前可以采用()在多項式級時間內求出旅行商問題的一個近似最優解。
A.回溯法
B.蠻力法
C.近似算法D.都不可能
6.下列敘述錯誤的是()。
A.概率算法的期望執行時間是指反復解同一個輸入實例所花的平均執行時間
B.概率算法的平均期望時間是指所有輸入實例上的平均期望執行時間
C.概率算法的最壞期望事件是指最壞輸入實例上的期望執行時間
D.概率算法的期望執行時間是指所有輸入實例上的所花的平均執行時間
7.下列敘述錯誤的是()。
A.數值概率算法一般是求數值計算問題的近似解
B.Monte Carlo 總能求得問題的一個解,但該解未必正確
C.Las Vegas 算法的一定能求出問題的正確解
D.Sherwood 算法的主要作用是減少或是消除好的和壞的實例之間的差別
8.近似算法和貪心法有什么不同?
9.給定能隨機生成整數1 到5 的函數rand5(),寫出能隨機生成整數1 到7 的函數rand7()。
1.12.2練習題參考答案
1.答:C。
2.答:B。
3.答:A。
4.答:D。
5.答:C。
6.答:對概率算法通常討論平均的期望時間和最壞的期望時間,前者指所有輸入實例上平均的期望執行時間,后者指最壞的輸入實例上的期望執行時間。答案為D。
7.答:一旦用拉斯維加斯算法找到一個解,那么這個解肯定是正確的,但有時用拉斯維加斯算法可能找不到解。答案為C。
8.答:近似算法不能保證得到最優解。貪心算法不一定是近似算法,如果可以證明
83




算法設計
決策既不受之前決策的影響,也不影響后續決策,則貪心算法就是確定的最優解算法。9.解:通過rand5()*5+rand5()產生6,7,8,9,…,26,27,28,29,30 這25個整
數,每個整數x 出現的概率相等,取前面3*7=21 個整數,舍棄后面的4 個整數,將{6,7,8}轉化成1,{9,10,11}轉化成2,以此類推,即由y=(x-3)/3 為最終結果。對應的程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
intrand5()
{int a=1,b=5;
return rand()%(b-a+1)+a;
}
intrand7()
{int x;
do
{
x=rand5()*5+rand5();
} while (x>26);
int y=(x-3)/3;
return y;
}
voidmain()
{srand((unsigned)time(NULL));
for (int i=1;i<=20;i++)
printf("%d ",rand7());
printf("\n");
//包含產生隨機數的庫函數
//產生一個[1,5]的隨機數
//產生一個[1,7]的隨機數//隨機種子
//輸出20個[1,7]的隨機數
}
上述程序的一次執行結果如圖1.56 所示。
圖1.56
程序執行結果
84