算法设计的任务是对各类具体问题设计良好的算法及研究设计算法的规律和方法。常用的算法有:穷举搜索法、递归法、回溯法、贪心法、分治法等。
自然语言、数学语言、伪代码、程序设计语言、流程图、表格、图示…
算法用某种程序设计语言的具体实现
行为特性设计----处理数据的步骤设计 (算法设计)
结构性设计----对输入输出数据存储结构的设计 (数据结构设计)
程序=算法+数据结构
时间复杂度主要指CPU使用的时间,空间复杂度主要指内存使用的量
最坏情况下的时间复杂性:.
T m a x ( N ) = max I ∈ D N T ( N , I ) = max I ∈ D N ∑ i = 1 k t i e i ( N , I ) = ∑ i = 1 k t i e i ( N , I ∗ ) = T ( N , I ∗ ) T_{max}(N)=\max \limits_{I\in D_N}{T(N,I)}=\max \limits_{I\in D_N}{\sum^k_{i=1}t_ie_i(N,I)}=\sum^k_{i=1}t_ie_i(N,I^*)=T(N,I^*) Tmax(N)=I∈DNmaxT(N,I)=I∈DNmax∑i=1ktiei(N,I)=∑i=1ktiei(N,I∗)=T(N,I∗)
最好情况下的时间复杂性:
T m a x ( N ) = min I ∈ D N T ( N , I ) = min I ∈ D N ∑ i = 1 k t i e i ( N , I ) = ∑ i = 1 k t i e i ( N , I ~ ) = T ( N , I ~ ) T_{max}(N)=\min \limits_{I\in D_N}{T(N,I)}=\min \limits_{I\in D_N}{\sum^k_{i=1}t_ie_i(N,I)}=\sum^k_{i=1}t_ie_i(N,\widetilde{I})=T(N,\widetilde{I}) Tmax(N)=I∈DNminT(N,I)=I∈DNmin∑i=1ktiei(N,I)=∑i=1ktiei(N,I )=T(N,I )
平均情况下的时间复杂性:
T a v g ( N ) = ∑ I ∈ D N P ( I ) T ( N , I ) = ∑ I ∈ D N P ( I ) ∑ i = 1 k t i e i ( N , I ) T_{avg}(N)=\sum_{I\in D_N}P(I)T(N,I)=\sum_{I\in D_N}P(I)\sum^k_{i=1}t_ie_i(N,I) Tavg(N)=∑I∈DNP(I)T(N,I)=∑I∈DNP(I)∑i=1ktiei(N,I)
其中 D N D_N DN是规模为 N N N的合法输入的集合; I ∗ I^* I∗是 D N D_N DN中使 T ( N , I ∗ ) T(N,I^*) T(N,I∗)达到 T m a x ( N ) T_{max}(N) Tmax(N)的合法输入; I ~ \widetilde{I} I 是中使 T ( N , I ~ ) T(N,\widetilde{I}) T(N,I )达到 T m i n ( N ) T_{min}(N) Tmin(N)的合法输入;而 P ( I ) P(I) P(I)是在算法的应用由出现输入 I I I的概率。
三种情况下的复杂性中,可操作性最好,最有实际价值的是最坏情况下的时间复杂性。
如果只考虑某种特定情况,如最好或最坏情况的时间复杂性,输入的“ I I I”就是一个常量,可省略。因而有: T = T ( N , I ) → T = T ( N ) T=T(N,I)\to T=T(N) T=T(N,I)→T=T(N)
利用某一算法处理一个问题规模为n的输入所需的时间,称为该算法的时间复杂性。记为 T ( n ) T(n) T(n)
(1) 最坏情况下的时间复杂性
T m a x ( n ) = max { T ( I ) ∣ s i z e ( I ) = n } T_{max}(n)=\max \{ T(I) | size(I)=n \} Tmax(n)=max{T(I)∣size(I)=n}
(2) 最好情况下的时间复杂性
T m i n ( n ) = min { T ( I ) ∣ s i z e ( I ) = n } T_{min}(n) = \min\{ T(I) | size(I)=n\} Tmin(n)=min{T(I)∣size(I)=n}
(3) 平均情况下的时间复杂性
T a v g ( n ) : ∑ s i z e ( I ) = n p ( I ) T ( I ) T_{avg}(n) : \sum_{size(I)=n}{p(I)T(I)} Tavg(n):∑size(I)=np(I)T(I)
其中I是问题的规模为n的实例,p(I)是实例I出现的概率
例:顺序查找
i=1;
while(i<=n&&L[i]!=x)i++;
if(i>n)i=0;
printf(i);
T ( L [ i ] = i i ∈ [ i . . . n ] T ( L [ i ] = n i > n T(L[i]=i\quad i\in [i...n]\qquad T(L[i]=n\quad i>n T(L[i]=ii∈[i...n]T(L[i]=ni>n
T a v g ( n ) : ∑ s i z e ( I ) = n p ( I ) T ( I ) = ( n + 1 ) / 2 T_{avg}(n): \sum_{size(I)=n}p(I)T(I)=(n+1)/2 Tavg(n):∑size(I)=np(I)T(I)=(n+1)/2
T m a x ( n ) = max { T ( I ) ∣ s i z e ( I ) = n } = n T_{max}(n)=\max\{ T(I) | size(I)=n \}=n Tmax(n)=max{T(I)∣size(I)=n}=n
T m i n ( n ) = 1 T_{min}(n)=1 Tmin(n)=1
(1) for / while循环
(2)嵌套循环
(3)顺序语句
(4) if-else语句
尽管可以进行精确分析运行时间,但没有必要算出额外的精确度
当输入规模大到一定程度,使得复杂度只与运行时间的增长量级有关时,则是在研究算法的渐进效率。
利用某一算法处理一个问题规模为n的输入所需的时间,称为该算法的时间复杂性。记为T(n)
当问题的规模递增时,时间复杂性的极限称为渐进时间复杂性。
当n趋近无穷时,T(n)渐近于t(n)
定义:
令f(n)和g(n)是从自然数集到非负实数集的二个函数。如果存在一个自然数 n 0 n_0 n0,和一个正常数 c c c,使得 n ≥ n 0 , f ( n ) ≤ c g ( n ) n≥n_0,f(n)≤cg(n) n≥n0,f(n)≤cg(n)则称 f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n))(理解为:f(N)的阶不高于g(N)的阶), n 0 n_0 n0称为阈值。
因此若 lim n → ∞ f ( n ) g ( n ) ≠ ∞ \lim_{n\to \infty} \frac{f(n)}{g(n)}\ne \infty limn→∞g(n)f(n)=∞(正常数,可以是0。)蕴含着 f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n))
一般地,对于足够大的n,常用的时间复杂性存在以下顺序:
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n ∗ l o g n ) < O ( n 2 ) < O ( n 3 ) . . . < O ( 2 n ) < O ( 3 n ) < . . . < O ( n ! ) O(1)< O(logn)< O(n)< O(n*logn)
O的运算规则:
(1) O ( f ) + O ( g ) = O ( m a x ( f , g ) ) ; O(f)+O(g)=O(max(f,g)); O(f)+O(g)=O(max(f,g));
●例: O ( n 2 ) + O ( n ) = O ( m a x ( n 2 , n ) ) ; O(n^2)+O(n)=O(max (n^2, n)); O(n2)+O(n)=O(max(n2,n));
(2) O ( f ) + O ( g ) = O ( f + g ) ; O(f)+O(g)=O(f+g); O(f)+O(g)=O(f+g);
●例: O ( n 2 ) + O ( n ) = O ( n 2 , n ) ; O(n^2)+O(n)=O(n^2, n); O(n2)+O(n)=O(n2,n);
(3) O ( f ) O ( g ) = O ( f g ) ; O(f)O(g)=O(fg); O(f)O(g)=O(fg);
●例: O ( n 2 ) O ( n ) = O ( n 2 ∗ n ) ; O(n^2)O(n)=O(n^2*n); O(n2)O(n)=O(n2∗n);
(4)如果 g ( N ) = O ( f ( N ) ) g(N)=O(f(N)) g(N)=O(f(N)),则 O ( f ) + O ( g ) = O ( f ) O(f)+O(g)=O(f) O(f)+O(g)=O(f);
●例: 2 n + 1 = O ( n ) 2n+1=O(n) 2n+1=O(n),则 O ( n ) + O ( 2 n + 1 ) = O ( n + 2 n + 1 ) O(n)+O(2n+1)=O(n+2n+1) O(n)+O(2n+1)=O(n+2n+1)
(5) O ( C f ( N ) ) = O ( f ( N ) ) O(Cf(N))=O(f(N)) O(Cf(N))=O(f(N)),其中C是一个正的常数;
●例: O ( 2 n ) = O ( n ) O(2n)=O(n) O(2n)=O(n)
(6) f = O ( f ) f=O(f) f=O(f)。
●例: n 2 = O ( n 2 ) n^2=O(n^2) n2=O(n2)
2 n + 1 = O ( 2 n + 1 ) 2n+1= O(2n+1) 2n+1=O(2n+1)
分治法将规模为n的问题分成k个规模为n/m的子问题:
T ( n ) = { O ( 1 ) n = 1 k T ( n / m ) + f ( n ) n > 1 T(n)= \left\{\begin{aligned}O(1)\qquad\qquad\qquad n=1 \\ kT(n/m)+f(n)\quad n>1 \end{aligned}\right. T(n)={O(1)n=1kT(n/m)+f(n)n>1 ==》 T ( n ) = n l o g m k + ∑ j = 0 l o g m n − 1 k j f ( n m j ) T(n)=n^{log_mk}+\sum_{j=0}^{log_mn-1}k^jf(\frac{n}{m^j}) T(n)=nlogmk+∑j=0logmn−1kjf(mjn)
直接或间接地调用自身的算法
用函数自身给出定义的函数
阶乘函数可递归地定义为:
n ! = { 1 n = 1 可视为 边界条件 n ( n − 1 ) ! n > 1 可视为 递归函数 n!= \left\{\begin{aligned} 1\qquad\qquad\ \ n=1\qquad 可视为\ 边界条件\\ n(n-1)!\quad n>1\qquad 可视为\ 递归函数 \end{aligned}\right. n!={1 n=1可视为 边界条件n(n−1)!n>1可视为 递归函数
//求解阶乘函数的递归算法
long Fac ( long n ) {
if ( n <= 1) return 1;
else return n * Fac (n-1);
}
无穷数列1,1,2,3,5,8,13,21,34,55,…,被称为Fibonacci数列。 ——1202《算盘之书》
它可以递归地定义为:
F ( n ) { 1 n = 0 1 n = 1 } 可视为 边界条件 F ( n − 1 ) + F ( n − 2 ) n > 1 } 可视为 递归函数 F(n)\left\{\begin{aligned}\ \left.\begin{aligned}\ 1 \qquad\qquad\qquad\qquad\quad n=0\\1 \qquad\qquad\qquad\qquad\quad n=1 \end{aligned}\right\}可视为\ 边界条件\\ \left.\begin{aligned}\ F(n-1)+F(n-2)\ \ \ n>1 \end{aligned}\right\}可视为\ 递归函数\\ \end{aligned}\right. F(n)⎩ ⎨ ⎧ 1n=01n=1}可视为 边界条件 F(n−1)+F(n−2) n>1}可视为 递归函数
//第n个Fibonacci数可递归地计算如下:
int fibonacci(int n)
{
if (n <= 1) return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
前2例中的函数都可以找到相应的非递归方式定义:
n ! = 1 ⋅ 2 ⋅ 3 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ( n − 1 ) ⋅ n n!=1·2·3······(n-1)·n n!=1⋅2⋅3⋅⋅⋅⋅⋅⋅(n−1)⋅n
F ( n ) = 1 5 ( ( 1 + 5 2 ) n + 1 − ( 1 − 5 2 ) n + 1 ) F(n)=\frac{1}{\sqrt{5}}\left( \left(\frac{1+\sqrt{5}}{2} \right)^{n+1}- \left(\frac{1-\sqrt{5}}{2} \right)^{n+1} \right) F(n)=51((21+5)n+1−(21−5)n+1)
但Ackerman函数却无法找到非递归的定义。
当一个函数及它的一个变量是由函数自身定义时,称这个函数是双递归函数。
Ackerman函数A(n,m)定义如下:
{ A ( 1 , 0 ) = 2 A ( 0 , m ) = 1 m ≥ 0 A ( n , 0 ) = n + 2 n ≥ 2 A ( n , m ) = A ( A ( n − a , m ) , m − 1 ) n , m ≥ 1 \left\{\begin{aligned}A(1,0)=2 \qquad\qquad\qquad\qquad\qquad\qquad\quad \\ A(0,m)=1\qquad\qquad\qquad\qquad\qquad\ m\ge 0\\ A(n,0)=n+2\qquad\qquad\qquad\qquad\ \ \ n\ge 2\\ A(n,m)=A(A(n-a,m),m-1)\quad n,m\ge 1 \end{aligned}\right. ⎩ ⎨ ⎧A(1,0)=2A(0,m)=1 m≥0A(n,0)=n+2 n≥2A(n,m)=A(A(n−a,m),m−1)n,m≥1
设计一个递归算法生成n个元素 r 1 , r 2 , … , r n {r_1,r_2,…,r_n} r1,r2,…,rn的全排列。例如R={a,b,c}的全排列:abc,acb,bac,bca,cab,cba 共6个。
设:
R = { r 1 , r 2 , … , r n } R=\{r_1,r_2,…,r_n\} R={r1,r2,…,rn}是要进行排列的n个元素, R i = R − { r i } R_i=R-\{r_i\} Ri=R−{ri}。
集合R中元素的全排列记为 p e r m ( R ) perm(R) perm(R)。
( r i ) p e r m ( R i ) {\color{red}{(r_i)}}perm(R_i) (ri)perm(Ri)表示在全排列 p e r m ( R i ) perm(R_i) perm(Ri)的每一个排列前加上前缀得到的排列。
R的全排列可归纳定义如下:
//perm 的递归算法如下:
void perm(int list[],int k,int m){
int i;
if(k==m){
for(i=0;i<=m;i++)
printf("%c ", list[i]);
}else{
for(i=k;i<=m;i++){
swap(& list[k],& list[i]);
perm(list,k+1,m);
swap(& list[k],& list[i]);
}
}
}
n = n 1 + n 2 + … + n k , 其中 n 1 ≥ n 2 ≥ … ≥ n k ≥ 1 , k ≥ 1 。 n=n_1+n_2+…+n_k,其中n_1≥n_2≥…≥n_k≥1,k≥1。 n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1。
解:
例如正整数6有如下11种不同的划分:
6;
5+1; 最大数5
4+2,4+1+1; 最大数4
3+3,3+2+1,3+1+1+1;最大数3
2+2+2,2+2+1+1,2+1+1+1+1; 最大数2
1+1+1+1+1+1。
设最大数为m,将最大加数 n i n_i ni 不大于m的划分个数记作q(n,m)。
(1) q(n,1)=1,n<=1;
当最大加数 n i n_i ni不大于1时,任何正整数n只有一种划分形式
(2) q(n,m)=q(n,n),m>n;
最大加数n1实际上不能大于n。因此,q(1,m)=1。
(3) q(n,n)=1+q(n,n-1);
正整数n的划分由m=n的划分和m≤n-1的划分组成。
(4) q(n,m)=q(n,m-1)+q(n-m,m),n>m>1;
正整数n的最大加数n1不大于m的划分由n1=m的划分和n1≤m-1 的划分组成。
因此,可以建立q(n,m)的如下递归关系:
q ( n , m ) = { 1 n = 1 或 m = 1 q ( n , n ) n < m 1 + q ( n , n − 1 ) n = m q ( n , m − 1 ) + q ( n − m , m ) n > m > 1 q(n, m) =\left\{\begin{aligned} 1\qquad\qquad\qquad\qquad\qquad n = 1或m = 1\\ q(n, n)\qquad\qquad\qquad\qquad\qquad\quad n
正整数n的划分数p(n)=q(n,n)。
void hanoi(int n, int a, int b, int c){
if (n > 0) {
hanoi(n-1, a, c, b);
move(a,b);
hanoi(n-1, c, b, a);
}
}
结构清晰;
可读性强;
容易用数学归纳法来证明算法的正确性;
为设计算法、调试程序带来很大方便。
运行效率较低(运行过程中调用自身),耗费的计算时间还是占用的存储空间都比非递归算法要多;
分治法所能解决的问题的特征:
第3条特征:能否利用分治法完全取决于问题是否具有这条特征,如果具备了前两条特征,而不具备第三条特征,则可以考虑贪心算法或动态规划。
第4条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然也可用分治法,但一般用动态规划较好。
分治模式在每一层递归上都有三个步骤:
divide-and-conquer(P){
if ( | P | <= n0)
adhoc(P); //解决小规模的问题
divide P into smaller subinstances P1,P2,...,Pk;//分解问题
// 分解问题
for (i=1,i<=k,i++)
yi=divide-and-conquer(Pi); //递归的解各子问题
return merge(y1,...,yk); //将各子问题的解合并为原问题的解
}
// 其中adhoc(P)为基本子算法,直接解小规模问题P
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。
设分解阀值n0=1 ,且adhoc解规模为1的问题耗费1个单位时间。
再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。
用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有: T ( n ) = { O ( 1 ) n = 1 k T ( n / m ) + f ( n ) n > 1 T(n)=\left\{\begin{aligned} O(1)\qquad\qquad\qquad n=1\\ kT(n/m)+f(n)\quad n>1 \end{aligned}\right. T(n)={O(1)n=1kT(n/m)+f(n)n>1
通过迭代法求得方程的解: T ( n ) = n l o g m k + ∑ j = 0 l o g m n − 1 k j f ( n m j ) T(n)=n^{log_mk}+\sum_{j=0}^{log_mn-1}k^jf(\frac{n}{m^j}) T(n)=nlogmk+∑j=0logmn−1kjf(mjn)
注意:递归方程及其解只给出 n n n等于 m m m的方幂时 T ( n ) T(n) T(n)的值,但是如果认为 T ( n ) T(n) T(n)足够平滑,那么由n等于m的方幂时 T ( n ) T(n) T(n)的值可以估计 T ( n ) T(n) T(n)的增长速度。通常假定 T ( n ) T(n) T(n)是单调上升的,从而当 m i ≤ n < m i + 1 m^i≤n
在分析复杂性时,通常会得到递归不等式:
T ( n ) < = { O ( 1 ) n = 1 k T ( n / m ) + f ( n ) n > 1 T(n)<=\left\{\begin{aligned} O(1)\qquad\qquad\qquad n=1\\ kT(n/m)+f(n)\quad n>1 \end{aligned}\right. T(n)<={O(1)n=1kT(n/m)+f(n)n>1
注意: 在讨论最坏情况下的复杂性时,用等号或者小于等于号没有本质区别。
给定已按升序排好序的 n n n个元素 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1],现要在这n个元素中找出一特定元素 x x x。
(1)该问题的规模缩小到一定的程度就可以容易地解决;
分析: 如果 n = 1 n=1 n=1即只有一个元素,则只要比较这个元素和 x x x就可以确定 x x x是否在表中。因此这个问题满足分治法的第一个适用条件
(2)该问题可以分解为若干个规模较小的相同问题; **
分析: 比较x和a的中间元素 a [ m i d ] a[mid] a[mid],若 x = a [ m i d ] x=a[mid] x=a[mid],则 x x x在 L L L中的位置就是 m i d mid mid;如果 x < a [ m i d ] xx<a[mid],由于 a a a是递增排序的,因此假如 x x x在 a a a中的话, x x x必然排在 a [ m i d ] a[mid] a[mid]的前面,所以我们只要在 a [ m i d ] a[mid] a[mid]的前面查找 x x x即可;如果 x > a [ i ] x>a[i] x>a[i],同理我们只要在 a [ m i d ] a[mid] a[mid]的后面查找 x x x即可。无论是在前面还是后面查找 x x x,其方法都和在** a a a中查找 x x x一样,只不过是查找的规模缩小了。
(3)分解出的子问题的解可以合并为原问题的解;
(4)分解出的各个子问题是相互独立的。
分析: 很显然此问题分解出的子问题相互独立,即在 a [ i ] a[i] a[i]的前面或后面查找 x x x是独立的子问题,因此满足分治法的第四个适用条件。
int BinarySearch(Type a[], int x, int l, int r)
{
while (l<=r){
int m = (l+r)/2;
if (x == a[m]) return m;
if (x < a[m]) r = m-1; else l = m+1;
}
return -1;
}
共n个元素,
每比较一次后的范围是: n / 2 , n / 4 , . . . . n / 2 k n/2,n/4,....n/2^k n/2,n/4,....n/2k,其中k就是循环的次数
因为 n / 2 k 取整 > = 1 n/2^k取整>=1 n/2k取整>=1
令 n / 2 k = 1 n/2^k=1 n/2k=1
则 k = l o g 2 n k=log_2n k=log2n
所以时间复杂度可以表示 O ( l o g n ) O(logn) O(logn),最坏情况下的时间复杂度是 O ( l o g n ) O(logn) O(logn)
思路:
拆分:
a 2 k − 1 a^{2^{k-1}} a2k−1 a 2 k − 1 a^{2^{k-1}} a2k−1
a 2 k − 2 a^{2^{k-2}} a2k−2 a 2 k − 2 a^{2^{k-2}} a2k−2 a 2 k − 2 a^{2^{k-2}} a2k−2 a 2 k − 2 a^{2^{k-2}} a2k−2
… … … … … … … …
a 2 k − k a^{2^{k-k}} a2k−k……………………… a 2 k − k a^{2^{k-k}} a2k−k
9223372036854775807 = 2 63 − 1 9223372036854775807 =2^{63}-1 9223372036854775807=263−1 ?
2 n , n = 9223372036854775807 2n,n= 9223372036854775807 2n,n=9223372036854775807 ?
分治法:
(1)
X=A B
Y=C D
X Y = A C 2 n + ( A D + B C ) 2 n / 2 + B D XY=AC 2^{n} +(AD+BC) 2^{n/2}+BD XY=AC2n+(AD+BC)2n/2+BD
AC AD BC BD 是四个子问题
T ( n ) = { O ( 1 ) n = 1 4 T ( n / 2 ) + O ( n ) n > 1 = O ( n 2 ) T(n)=\left\{\begin{aligned} O(1)\qquad\qquad\qquad n=1\\ 4T(n/2)+O(n)\quad n>1 \end{aligned}\right. =O(n^{2}) T(n)={O(1)n=14T(n/2)+O(n)n>1=O(n2)
细节问题: 两个XY的复杂度都是O(nlog3),但考虑到A+B,C+D可能得到m+1位的结果,使问题的规模变大,故选择第1种XY方案。
Expected node of symbol group type, but got node of type cr = O ( n l o g 3 ) = O ( n 1.59 ) =O(n^{log3})=O(n^{1.59}) =O(nlog3)=O(n1.59)
(3)
两个n×n矩阵A和B的乘积矩阵C中元素C[i,j]定义为: c [ i ] [ j ] = ∑ k = 1 n A [ i ] [ k ] B [ k ] [ j ] c[i][j]=\sum_{k=1}^nA[i][k]B[k][j] c[i][j]=∑k=1nA[i][k]B[k][j]
若依此定义来计算A和B的乘积矩阵C,则每计算C的一个元素C[i][j],需要做n次乘法和n-1次加法。因此,算出矩阵C的 个元素所需的计算时间为 O ( n 3 ) O(n^3) O(n3)。
两个矩阵相乘的经典算法若设 Q=MN其中,M是n1n1矩阵,N是n2*n2矩阵。当n1=n2时有:
for(i=1;i<=n1;++i){
for(j=1;j<=n2;++j){
q[i][j]=0;
for(k=1;k<=n1;++k)
q[i][j]+=m[i][k]*n[k][j];
}
}
(1)
**将矩阵A,B和C中每一矩阵都分块成4个大小相等的子矩阵。由此可将方程C=AB重写为: **
**由此可得: **
(8个子问题)
T ( n ) = { O ( 1 ) n = 2 8 T ( n / 2 ) + O ( n 2 ) n > 2 = O ( n 3 ) T(n)=\left\{\begin{aligned} O(1)\qquad\qquad\qquad n=2\\ 8T(n/2)+O(n^2)\quad n>2 \end{aligned}\right. =O(n^3) T(n)={O(1)n=28T(n/2)+O(n2)n>2=O(n3)
没有改进
(2)将其分成7个子问题
等同于:
Expected node of symbol group type, but got node of type cr
(3)
归并排序中,我们会先找到一个数组的中间下标mid,然后以这个mid为中心,对两边分别进行排序,之后我们再根据两边已排好序的子数组,重新进行值大小分配。
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int l1 = l;
int r1 = m + 1;
while (l1 <= m && r1 <= r) {
help[i++] = arr[l1] < arr[r1] ? arr[l1++] : arr[r1++];
}
while (l1 <= m) {
help[i++] = arr[l1++];
}
while (r1 <= r) {
help[i++] = arr[r1++];
}
for (int j = 0; j < help.length; j++) {
arr[l + j] = help[j];
}
}
}
从问题的初始状态出发,通过若干次的贪心选择得出问题的最优解或近似优解的一种解题策略。
指的是从对问题的某一初始解出发,一步一步的攀登给定的目标,尽可能快地去逼近更好的解。当达到某一步,不能再攀登时,算法便终止。
贪心算法总是做出在当前看来是最好的选择,它并不是从总体最优上加以考虑,他所作出的选择只是在某种意义上的局部最优选择。能够得到的解不一定是最优解。
是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
贪心算法通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,做出贪心选择后,原问题简化为规模更小的类似子问题。必须证明每一步所作的贪心选择最终导致问题的整体最优解。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可**用动态规划算法**或**贪心算法**求解的关键特征。
高效地安排一系列争用某一公共资源的活动。 如演讲会场等
即:在所给的活动集合中选出最大的相容活动子集合。
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi , 且si
若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。也就是说,当si≥fj或sj≥fi时,活动i与活动j相容。
求:所给活动集合中最大相容活动子集合
考虑贪心标准是什么?即,是先选择活动持续时间最短的活动占用资源?还是以结束时间最早的活动占用资源?
答:以结束时间最早的活动占用资源。先以结束时间f[i]排序,先选择结束越早的,之后的留下的时间一定是最多的,更利于更多的时间安排。
例:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
S[i] | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
f[i] | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
//事先有排序算法
//各活动的起始时间和结束时间存储于数组s和f中且按结束时间的非减序排列
void GreedySelector(int n, Type s[], Type f[], bool A[])
{
A[1]=true;
int j=1;
for (int i=2;i<=n;i++)
{//发现一个活动的起始时间比当前活动的结束时间晚——局部最优
if (s[i]>=f[j]) { A[i]=true; j=i; }
else A[i]=false;
}
}
第一步:总存在以贪心选择开始的最优活动安排方案
设E= {1,2,…, n}为所给的活动集合,且E中活动按照结束时间非减序排列,故 活动1具有最早完成时间。
首先,证明活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1.
设A属于E是所给活动安排问题的一个最优解,且A中活动也按结束时间非减序排列,A中的第一个活动是k.
若k=1,则A就是一个以贪心选择开始的最优解; 若k>1,则设B=A-{k}U{1}。
由于f1
在做了第一步贪心选择活动1后,原问题简化为对E中所有与活动1相容的活动进行活动安排的子问题。
即若A是原问题的最优解,则A’=A-{1}是活动安排问题E’={i属于E:si>=f1}的最优解。
反证法: 假设E’有一个解B’,包含比A’更多的活动 B=B’ U{1},则B比A包含更多的活动 这与A的最优性矛盾
结论:每一步所做的贪心选择都将原问题简化为一个更小的与原问题具有相同形式的子问题。
由于输入的活动以其完成时间的非减序排列,所以算法greedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。
按这种方法选择相容活动为未安排活动留下尽可能多的时间。** 即该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。
算法greedySelector的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)**的时间安排n个活动,使最多的活动能相容地使用公共资源。
对于活动安排问题,贪心算法却总能求得整体最优解,即最终所确定的相容活动集合A的规模最大。
有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。
void Loading(int x[], Type w[], Type c, int n)
{
int t[n+1];
Sort(w, t, n);//对w进行排序,将排序后的角标存入t,而w不变
for (int i = 1; i <= n; i++)
x[i] = 0;
for (int i = 1; i <= n && w[t[i]] <= c; i++)
{
x[t[i]] = 1;
c -= w[t[i]];
}
}
设集装箱依其重量从小到大排序,(x1, x2, …, xn)是最优装载问题的一个最优解。
如果给定的最优装载问题有解,则 1<=k<=n
设(x1, x2, …, xn)是装载问题的满足贪心选择性质的最优解,则x1=1,且( x2, …, xn)是轮船载重量为c-w1、待装船集装箱为{2, 3, …, n}时相应最优装载问题的最优解。即最优装载问题具有最优子结构性质。
由最优装载问题的贪心选择性质和最优子结构性质,容易证明算法loading的正确性。算法loading的主要计算量在于将集装箱依其重量从小到大排序,故算法所需的计算时间为 O(nlogn)。
0-1背包问题:
给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
(在选择装入背包的物品时,对每种物品i只有2种选择,即不装入(0)或装入背包(1)背包。 不能将物品i装入背包多次,也不能只装入部分的物品i。 )
分数背包问题:
与0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,1≤i≤n。
这2类问题都具有最优子结构性质,极为相似,但背包问题可以用贪心算法求解,而0-1背包问题却不能用贪心算法求解。
在背包容量占用速率和背包效益值的增长速率之间取得平衡。 即每次选择装入背包的物品,应满足它所占用背包的每一单位容量能获得当前最大的单位效益。
以目标函数作为量度标准——价值最大装入背包
存在问题: 背包效益值得到最大增加的同时,背包容量被过快占用。
以物品质量作为量度标准——质量最轻装入背包
存在问题: 背包容量消耗慢,但却无法保证背包的效益值增加快。
以物品单位质量价值(Vi /Wi)作为量度标准 ——单位质量价值最大装入背包
分析:
- 满足最优子结构性质
- 满足贪心选择性质
- 以单位质量价值做为量度标准进行选择时,相当于把每一个物品都分割成单位块,单位块的利益越大,显然,物品装入背包后,背包获取总效益越大。
结论:
- 以单位质量价值作为贪心标准求解(分数)背包问题能够得到一个最优解。
void Knapsack(int n,float M,float v[],
float w[],float x[])
{ Sort(n,v,w);
int i;
for (i=1;i<=n;i++) x[i]=0;
float c=M;
for (i=1;i<=n;i++) {
if (w[i]>c) break;
x[i]=1;
c-=w[i];
}
if (i<=n) x[i]=c/w[i];
}
算法knapsack的主要计算时间在于将各种物品依其单位重量的价值从大到小排序。因此,算法的计算时间上界为O(nlogn)
对于0-1背包问题 :
它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。
事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。
哈夫曼提出构造最优前缀码的贪心算法,由此产生的编码方案称为哈夫曼编码。
哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。
算法以|C|个叶结点开始,执行|C|-1次的“合并”运算后产生最终所要求的树T。
电文发送过程:
前缀编码:
对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀,这种编码称为前缀(编)码
最优前缀编码:
平均码长或文件总长最小的前缀编码称为最优的前缀编码
略
用带权的有向图表示一个交通运输网,图中:
顶点——表示城市
边——表示城市间的交通联系
权——表示此线路的长度或沿此线路运输所花的时间或费用等
问题:从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径——最短路径
给定带权有向图G =(V,E),其中每条边的权是非负实数。另外,还给定V中的一个顶点,称为源。现在要计算从源到所有其它各顶点的最短路长度。这里路的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。
Dijkstra算法是解单源最短路径问题的贪心算法。
基本思想:
设置顶点集合S并不断地作贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。
初始时,S中仅含有源。设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。
Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其它顶点之间的最短路径长度。 (3)
输入带权有向图G=(V,E), V={1,2,…,n},顶点V1是源
C[i][j]表示边(i,j)的权
dist[i]表示从源到顶点vi的最短特殊路径长度
prev[i]表示从源到顶点i的最短路径上i的前一个顶点。
输入参数:n , v1 , c[i][j]
输出参数:dist[i], prev[i],s[i]
void Dijkstra(int n,int v, type dist[],int prev[],type**c)
{
bool s[maxint];
for(i=1;i<=n;i++)
{
s[i]=false;
dist[i]=c[v][i];
if (dist[i]==maxint)
prev[i]=0;
else
prev[i]=v;
}
s[v]=true;
dist[v]=0;
for(j=1;j<n;j++){
temp=maxint;
u=v;
for(j=1;j<=n;j++)
{
if(!s[j]&&dist[j]<temp)
{
u=j;
temp=dist[j];
}
for(j=1;j<=n;j++){
if(!s[j]&&c[u][j]<maxint)
{
newdist=dist[u]+c[u][j];
if(newdist< dist[j])
{
dist[j]=newdist;prev[j]=u;
}
}
}
}
}
}
对于具有n个顶点和e条边的带权有向图,如果用带权邻接矩阵表示这个图,那么Dijkstra算法的主循环体需要O(n)时间。这个循环需要执行n-1次,所以完成循环需要O(n2)时间。算法的其余部分所需要时间不超过O(n2)。
n个城市间,最多可设置n(n-1)/2条线路
n个城市间建立通信网,只需n-1条线路
问题转化为:如何在可能的线路中选择n-1条,能把所有城市 (顶点)均连起来,且总耗费(各边权值之和)最小
设G =(V,E)是无向连通带权图,即一个网络。E中每条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树。
设G=(V,E)是一个连通网络,U是顶点集V的一个非空子集。若(u,v)是G中所有的一个端点在U(u∈U)里、另一个端点不在U(即v∈V-U)里的边中,并且是具有最小权值的一条边,则一定存在G的一棵最小生成树包括此边(u,v)。
设G=(V,E)是连通带权图,V={1,2,…,n}。
构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件i属于S,j属于V-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。
在这个过程中选取到的所有边恰好构成G的一棵最小生成树。
用普里姆算法
void PRIM( MGraph G,VertexType u) //从第u个顶点出发
{ k=LocateVex(G,u);
for (j=0; j<G.vexnum; j++)
if(j!=k) closedge[j]={u, G.arcs[k][j].adj};
closedge[k].lowcost=0; //初始U={u}
for (i=1; i<G.vexnum; i++) // 循环n-1次,每次求出最小生成树的一条边
{ k=minnum(closedge);
printf(closedge[k].adjvex,G.vexs[k]);
//输出生成树的边
closedge[k].lowcost=0; // 将 k 并入U 中
for (j=0; j<G.vexnum; j++) // 修改 lowcost[ ] 和closest[ ]
if (G.arcs[k][j].adj< closedge[j].lowcost) //新顶点并入后重新选择最小边
lowedge[j]={u, G.arcs[k][j].adj};
}
}
普里姆(Prim)算法算法适合求边稠密的网的最小生成树
克鲁斯卡尔(Kruskal)算法适合求边稀疏网的最小生成树
多机调度问题要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。
约定,每个作业均可在任何一台机器上加工处理,但未完工前不允许中断处理。作业不能拆分成更小的子作业。
这个问题是NP完全问题,到目前为止还没有有效的解法。对于这一类问题,用贪心选择策略有时可以设计出较好的近似算法。
采用最长处理时间作业优先的贪心选择策略可以设计出解多机调度问题的较好的近似算法。
按此策略,当n<=m (作业数少于机械数)时,只要将机器i的[0, ti]时间区间分配给作业i即可,算法只需要O(1)时间。
当 n>m(作业数多于机器数)时,首先将n个作业依其所需的处理时间从大到小排序。然后依此顺序将作业分配给空闲的处理机。算法所需的计算时间为O(nlogn)。
例如:
设7个独立作业{1,2,3,4,5,6,7}由3台机器M1,M2和M3加工处理。各作业所需的处理时间分别为{2,14,4,16,6,5,3}。
按算法greedy产生的作业调度如下图所示,所需的加工时间为17。
保存已解决的子问题的答案,在需要时再找出已求得的答案,避免大量重复计算,从而得到多项式时间算法。
动态规划算法对每个子问题只计算一次,不管该子问题是否以后会被用到,都将其结果保存到一张表中,从而避免每次遇到各个子问题时重新计算答案。
动态规划常用于求解最优解问题.
动态规划法的关键就在于: 对于重复出现的子问题,只 在第一次遇到时加以求解, 并把答案保存起来,让以后 再遇到时直接引用,不必重 新求解。
动态规划—Dynamic Programming =分治思想 + 解决冗余
f1=1,f2=1,f3=2,f4=3,f5=5,f6=8,f7=13, … … 序列中的每一个数是它前面二个数的和。这个序列递归定义如下:
F ( n ) { 1 n = 0 1 n = 1 } 可视为 边界条件 F ( n − 1 ) + F ( n − 2 ) n > 1 } 可视为 递归函数 F(n)\left\{\begin{aligned}\ \left.\begin{aligned}\ 1 \qquad\qquad\qquad\qquad\quad n=0\\ 1 \qquad\qquad\qquad\qquad\quad n=1 \end{aligned}\right\}可视为\ 边界条件\\ \left.\begin{aligned}\ F(n-1)+F(n-2)\ \ \ n>1 \end{aligned}\right\}可视为\ 递归函数\\ \end{aligned}\right. F(n)⎩ ⎨ ⎧ 1n=01n=1}可视为 边界条件 F(n−1)+F(n−2) n>1}可视为 递归函数
使用递归会产生巨大数量的重复调用
如果用数组F[1…n]来存储Fibonacci序列的值,由此得出自下而上的递推计算过程。
int Fibonacci(n)
{
F[1]=1;
F[2]=1;
for(i=3;i<=n;i++)
F[i]=F[i-1]+F[i-2];
return F[n];
}
问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。
在分析问题的最优子结构性质时,所用的方法是反证法
利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解
最优子结构是问题能用动态规划算法求解的前提。
递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。这种性质称为子问题的重叠性质。
动态规划算法,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。
通常不同的子问题个数随问题的大小呈多项式增长。因此用动态规划算法只需要多项式时间,从而获得较高的解题效率。
略
略
子序列:给定序列 X = x 1 , x 2 , … , x m X={x_1,x_2,…,x_m} X=x1,x2,…,xm,另一序列 Z = z 1 , z 2 , … , z k Z={z_1,z_2,…,z_k} Z=z1,z2,…,zk,若存在一个严格递增下标序列 i 1 , i 2 , … , i k {i_1,i_2,…,i_k} i1,i2,…,ik使得对于所有j=1,2,…,k有: z j = x i j z_j=x_{i_j} zj=xij,则Z是X的子序列。
公共子序列:给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。
问题:求找出一个最长公共子序列。
void LCSLength(int m,int n, char *x,char *y, int **c, int **b)
{
for (int i = 0; i <= m; i++) c[i][0]=0;
for (int i = 1; i <= n; i++) c[0][i]=0;
for (int i = 1; i <= m; i++){
for (int j = 1; j <= n; j++) {
if (x[i]==y[j]) {
c[i][j]=c[i-1][j-1]+1;
b[i][j]=‘↖’;
}else if (c[i-1][j]>=c[i][j-1]) {
c[i][j]=c[i-1][j];
b[i][j]=‘↑’;
}else {
c[i][j]=c[i][j-1];
b[i][j]= ‘←’;
}
}
}
}
时间复杂度:O(n+m)
问题: 给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
算法:
Void knapsack(int *v, int *w,int c,int n,int **m)//m(i,j)是背包容量为j,可选择物品为i,i+1,…,n时0-1背包问题的最优值,即最大价值和。
{
int jMax=min(w[n]-1,c);
for(j=0;j<=jMax; j++) m[n][j]=0;//第n个物品无法装入
for(j=w[n]; j<=c; j++) m[n][j]=v[n];//装入第n个物品
for(i=n-1;i>1;i--){
jMax=min(w[i]-1,c);
for(j=0;j<= jMax;j++) //无法装入第i个物品
m[i][j]=m[i+1][j];
for(j= w[i];j<=c;j++) //可以装入第i个物品
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]) //可以装入第1个物品
m[1][c]=max(m[1][c], m[2][c- w[1]]+v[1]);
}
**问题:**设有投资公司,在3月份预计总投资额为m万元,共有n个项目,Gi(x)为向第i项工程投资费用为x万元时的预计收益,如何分配资源才能获得最大利润?
设总投资额为m万元,共有n个项目, Fn(m)为向n个项目投资m万元所获最大收益, Gi(xi)为向第i项工程投资xi时的收益,则有:
Invest(m,n,f[n][m],g[n][m],d[n][m]){ //d[i][j]:前i项工程投资额为j时,向第i项工程的投资额
//向前i项工程投资额为j时,f[i][j]获得最大收益
for(j=0;j<=m;j++){ //只投资第1项工程,投资额为j时,获得最大收益
f[1][j]=g[1][j];
d[1][j]=j;
}
for(i=2;i<=n;i++){
for(j=0;j<=m;j++){
f[i][j]=0;
for(k=0;k<=j;k++){
s=f[i-1][j-k]+g[i][k];
if(s>f[i][j]) {
f[i][j]=s;
d[i][j]=k;
}
}
}
}
}
时间复杂度分析: O ( n m 2 ) O(nm^2) O(nm2)
空间复杂度分析 : O ( n m ) O(nm) O(nm)
回溯法(也称试探法):将问题候选解按某一顺序逐一枚举和试探的过程。
可行解:满足约束条件的解。解空间中的一个子集 。
最优解:使目标函数取极值(极大或极小)的可行解,一个或少数几个 。
扩展结点:一个正在产生儿子的结点称为扩展结点
活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点
死结点:一个所有儿子已经产生的结点称做死结点
问题的解空间: 对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
例:n=3的0-1背包问题
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2, …, xn)的形式。
显约束:对分量xi的分量限定。
隐约束:为满足问题的解而对不同分量之间施加的约束。
针对所给问题,定义问题的解空间;
确定易于搜索的解空间结构;
以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
剪枝函数,可避免无效搜索,提高回溯法的搜索效率。
用约束函数在扩展结点处剪去不满足约束的子树;(背包问题中大于背包容量)
用限界函数剪去得不到最优解的子树;(最大价值)
–具有剪枝函数的深度优先生成法称为回溯法。
用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根节点到当前扩展结点的路径。如果解空间树中从根节点到叶节点的最长路径的长度为h(n),则回溯法所需要的计算空间通常为O(h(n)),而显示的存储整个解空间则需要O(2^h(n))或O(h(n)!)内存空间。
回溯法对解空间作深度优先搜索,因此,在一般情况下用递多一点。
采用树的非递归深度优先遍历算法,可将回溯法表示为一个非递归迭代过程。
子集树:当所给问题是从n个元素的集合S中找出S满足某种性质的子集时,相应的解空间树称为子集树。如:n个物品的0-1背包问题所相应的解空间树。
遍历子集树需 O ( 2 n ) O(2^n ) O(2n)计算时间。
排列树:当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。如:旅行售货员问题的解空间树。
遍历排列树需要O(n!)计算时间。
不同列: xi !=xj
不处于同一正、反对角线: |i-j| != |xi-xj| (如何推算?)
(3)递归算法
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;
}
void Queen::Backtrack(int t)//t行
{
if (t>n) sum++;输出解;
else{
for (int i=1;i<=n;i++) {
x[t]=i;//设定列值,选取扩展结点
if (Place(t))
Backtrack(t+1);//试探第t+1行
}
}
}
backtrack(){
X[1]=0; int k=1;
While(k>0){
x[k]+=1;
while((x[k]<=n)&&!(place(k)))
x[k]+=1;
if(x[k]<=n)
if(k==n)
sum++;输出解;
else{
k++;
x[k]=0;
}
else {
x[k]=0;
k--;
}
}
}
Place(int k){
for(int j=1;j<k;j++){
if((abs(k-j)==aba(x[j]-x[k])) ||(x[j]==x[k]))
return false;
}
return true;
}
给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。这个问题是图的m可着色判定问题。
若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。
•解向量:(x1, x2, … , xn)表示顶点i所着颜色x[i]
•可行性约束函数:顶点i与已着色的相邻顶点颜色不重复。
•解空间树:n=3,m=3
void backtrack(int t){//递归算法
//t->节点t
if(t>n){
sum++;
//输出解
// for(int i=1;i<=n;i++)
//cout<
}else{
for(int i=1;i<=m;i++){
x[t]=i;
if(ok(t))
backtrack(t+1);//试探第t+1个节点
x[t]=0;
}
}
}
bool ok(int k){
for(int j=1;j<=n;j++){
if(a[k][j]&&(x[j]==x[k]))//如果两图相连并且颜色相同
return false;
return true;
}
M_coloring(int n, int m, int g[][]) { //非递归算法1
for(i=1;i<=n;i++)
x[i]=1;
k=1;
do{
if(x[k]<=m){
for (i=1;i<k;i++) {
if(g[i][k]==1 and x[i]==x[k]) break;
}
if(i<k)
x[k]++;
else
k++;
}else{
x[k]=1;
k=k-1;
if(k>=1)
x[k]++;
}
}while(k<=n and k>=1)
if(k>n)
输出解;
if(k<1) 无解;
}
M_coloring(int n, int m, int g[ ][ ]){ //非递归算法2
for(i=1;i<=n;i++) x[i]=0;
k=1;
while(k>0){
x[k]=x[k]+1;
while(x[k]<=m&&!color(k))
x[k]=x[k]+1;
if(x[k]<=m){
if(k==n){
输出x[ ];
break;
}else //试探
k++;
}else{//回溯
x[k]=0;
k=k-1;
}
}
}
Bool Color(k){
for(i=1;i<=n;i++)
if(g[i][k]==1 and x[i]==x[k])
return(false);
return(true);
}
上界函数bound(): 当前价值cw+剩余容量可容纳的最大价值<=当前最优价值bestp。
解向量: ( x 1 , x 2 , … , x n ) (x_1, x_2, … , x_n) (x1,x2,…,xn)
显约束: x i = 1 , 0 x_i=1,0 xi=1,0
隐约束: ∑ i = 1 n w i x i < = c , x i ∈ { 0 , 1 } , 1 < = i < = n \sum ^n_{i=1}{w_ix_i<=c} \ ,\ x_i\in\{0,1\},1<=i<=n ∑i=1nwixi<=c , xi∈{0,1},1<=i<=n
目标函数: ∑ i = 1 n v i x i \sum ^n_{i=1}{v_ix_i} ∑i=1nvixi
其它:装载问题\旅行商问题\迷宫问题\子集和数问题
回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费(或最大效益)优先的方式搜索解空间树。