1.算法的基本概念
广义:在解决问题时,按照某种机械步骤一定可以得到问题结果(有解时给出问题的解,无解时给出无解的结论)的处理过程。
狭义:用计算机解决问题的方法和步骤的描述。
程序=数据结构+算法
算法是一组有穷的规则,它规定了解决某一特定类型问题的一系列运算
算法的五个重要特性 :确定性、能行性、输入、输出、有穷性/有限性
1)确定性:算法每种运算必须有确切定义,不能有二义性
例:不符合确定性的运算:①5/0 ②将6或7与x相加 ③未赋值变量参与运算
2)能行性:算法中有待实现的运算都是基本的运算,原理上每种运算都能由人用纸和笔在有限的时间内完成
例:整数的算术运算是“能行”的,实数的算术运算是“不能行”的
3)输入:每个算法有0个或多个输入
4)输出:一个算法产生一个或多个输出
5)有穷性/有限性:一个算法总是在执行了有穷步的运算之后终止
计算过程:只满足确定性、能行性、输入、输出四个特性但不一定能终止的一组规则。
而算法是“可以终止的计算过程”
2.时间复杂度和空间复杂度
常见的多项式限界函数有:
Ο(1) < Ο(logn) < Ο(n) < Ο(nlogn) < Ο(n^2) < Ο(n^3)
常见的指数时间限界函数:
Ο(2^n) < Ο(n!) < Ο(n^n)
例:一个楼有n个台阶,有一个人上楼有时一次跨一个台阶,有时一次跨两个台阶,编写一个算法,计算此人有几种不同的上楼方法,并分析算法的时间复杂度。
设计一个递归算法。
H(int n){
if (n<0) printf(“Error!”);
if n=1 return(1);
if n=2 return(2);
else return(H(n-1)+H(n-2));
}
T(n)= T(n-1)+T(n-2) n>2
T(n) ≤2T(n-1) ≤ 2^2T(n-2) ≤ …≤ 2^(n-1)T(1) =O(2^n)
3.欧几里德算法(辗转相除法)
问题
输入:正整数m和n
输出:m和n的最大公因子
算法设计
①如果n = 0, 计算停止返回m, m即为结果;否则继续②
②记r为m除以n的余数,即r=m % n
③把n赋值给m,把r赋值给n,继续①
伪代码如下(循环):
Euclid(m, n){
while n<>0{
r = m % n;
m = n;
n = r;
}
}
递归代码:
// 约定m>n
GCD(m,n) {
if n=0 return(m)
else return (GCD(n,m mod n))
}
4.开灯问题
问题
有从1到n依次编号的n个同学和n 盏灯。
1号同学将所有的灯都关掉;
2号同学将编号为2的倍数的灯都打开;
3号同学则将编号为3的倍数的灯作相反处理(该号灯如打开的,则关掉;如关闭的,则打开); 以后的同学都将自己编号的倍数的灯,作相反处理。
问:经n个同学操作后,哪些灯是打开的?
算法设计
①被按了奇数次的灯泡是亮着的,被按了偶数次的灯泡是灭的
②可以发现,如果一个灯泡的编号具有偶数个因子,那么该灯泡就被按了偶数次,反之按了奇数次
③而只有完全平方数才有奇数个因子
④所以,原始问题可以转化为判断每个整数是否是完全平方数
5.贪婪算法
贪婪算法适用两类问题:
①可绝对贪婪问题
求最优解; 具有贪婪选择性质、最优子结构性质
②相对贪婪问题
求近似解; 不具备全部性质,但求解效率要求高,解的精度要求不高
贪婪选择性质:指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
最优子结构性质:当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
①币种统计问题
问题
某单位给每个职工发工资(精确到元)。为了保证不要临时兑换零钱,且取款的张数最少,取工资前要统计出所有职工的工资所需各种币值(100,50,20,10,5,2,1元共七种)的张数。请编程完成。
GZ\币值 | 100 | 50 | 20 | 10 | 5 | 2 | 1 | |
周 | 252 | 2 | 1 | 1 | ||||
张 | 2686 | 26 | 1 | 1 | 1 | 1 | 1 | |
…… | ||||||||
总计 | 2938 | 28 | 2 | 1 | 1 | 1 | 1 | 1 |
算法设计
①将七种币值存储在数组B。七种币值就表示为B[i],i=1,2,3,4,5,6,7
为了能实现贪婪策略,七种币应从大面额的币种到小面额的币种依次存储
②用一个长度为7的数组S记录最终币值所需数量
③贪婪策略: 对每个人的工资,用“贪婪”的思想,先尽量多地取大面额的币种,由大面额到小面额币种逐渐统计。
④算法的时间复杂度:O(n)
⑤某国的币种是这样的,共9种:100,70,50,20,10,7,5,2,1。
这种情况下,刚才的贪婪算法就无法求得最优解了
如某人工资是140,按贪婪算法140=100*(1张)+20*(2张)共需要3张,而事实上,只要取2张70面额的是最佳结果。7、70破坏了“取最优”的贪婪策略的正确性
②活动安排问题
问题
n个活动E={1,2,..,n},都要求使用同一公共资源(如演讲会场等)。且在同一时间仅一个活动可使用该资源。
活动时间[si,fi), si为起始时间, fi为结束时间。si<fi
活动i和j相容: si>=fj或sj > =fi
求最大的相容活动子集合,尽可能多的活动兼容使用公共资源
算法设计
①按结束时间非减序排序:f1<=f2 <= .. <= fn
②从第1个活动开始,按顺序放进集合A。放入活动i当且仅当与集合A中已有元素相容
与集合A中最后元素j比较:若si>=fj。则加入,否则不加入
fj=max(fk) ,k属于A,fj是集合A中的最大结束时间
③思想:选择具有最早结束时间的相容活动加入,使剩余的可安排时间最大,以安排尽可能多的活动。 由于输入的活动以其完成时间的非减序排列,所以算法GreedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。
④贪心算法并不总能求得问题的整体最优解。但对于这个问题总能求得最优解
//各活动的起始时间和结束时间存储于数组s和f中,且按结束时间的非减序排列
void GreedySelector(int n, int s[], int f[],bool A[]){
A[1]=true;// A[i]=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;
}
}
}
③埃及分数问题
问题
设计一个算法, 把一个真分数表示为埃及分数之和的形式。
所谓埃及分数,是指分子为1的分数。如:7/8=1/2+1/3+1/24。
算法设计
设该真分数为f
①找最小的n(最大的埃及分数1/n),使分数f>1/n
②输出1/n
③计算f=f-1/n
④若此时的f是埃及分数,输出f,算法结束,否则返回①
⑤基本思想:逐步选择分数所包含的最大埃及分数,这些埃及分数之和就是问题的一个解。
数学模型
①记真分数F=A/B;对B/A进行整除运算,商为D,余数为0<K<A,它们导出关系如下: B=A*D+K,B/A=D+K/A<D+1,A/B>1/(D+1),记C=D+1
②这样分数F所包含的“最大”埃及分数就是1/C。
③进一步计算:A/B-1/C=(A*C-B)/B*C
也就是说继续要解决的是有关分子为A=A*C-B,分母为B=B*C的问题
④Dijkstra 算法(解决单源最短路径问题)
问题
给定一个带权有向图G=(V,E),V={1,2,…,n },其中每条边的权是一个非负实数。
给定一个顶点 v,称为源。
单源最短路径问题:求从源到各顶点的最短路长度(路径上各边权之和)。
算法设计
①把 V 分成两组
S:已求出最短路径的顶点的集合
V - S = T:尚未确定最短路径的顶点集合
②初始时令 S={v0}, T={其余顶点},用辅助数组 D 存放v0到各顶点间的距离
D[i] 初值:若 <v0, vi> 存在,则为其权值;否则为∞
③从 T 中选取一个其距离值最小(也就是数组D中最小的那个)的顶点 vj,加入 S
④对 T 中顶点到v0的距离值进行修改:若 vj 作中间顶点,从 v0 到 vi 的距离值比不加 vj 的路径要短,则修改D[j]
⑤重复上述步骤,直到 S = V 为止
6.动态规划的手工计算
①资源分配问题
问题
设有资源n(n为整数),分配给m个项目,gi(x)为第i个项目分得资源x(x为整数)所得到的利润。
求总利润最大的资源分配方案,也就是解下列问题:
max z=g1(x1)+ g2(x2)+……gm(xm)
x1+x2+x3+……xm = n,0≤xi≤n,i=1,2,3,……,m
函数gi(x)以数据表的形式给出。
手工计算
例如:现有7万元投资到A,B,C 三个项目,利润见表,问题:求总利润最大的资源分配方案。
①资源:n = 7(万元);项目数:m = 3;gi(x)为第i个项目分得资源x所得到的利润。
②max z=g1(x1)+ g2(x2)+g3(x3)
x1+x2+x3= 7,0≤xi≤7,i=1,2,3
③划分阶段或找到子问题:每阶段增加一个项目,考察投资利润情况
④选择状态:每个阶段可以得到的最大利润;在第i阶段:前i个项目通过投资组合可得的最大利润;投资额不定:第i阶段总投资额为x时,设fi(x)为最大利润
⑤确定决策并写出状态转移方程
f1(x) = g1(x)
fi(x) = max{gi(xi) + fi-1(x-xi)}
gi(x):i项目投x后可得利润 fi(x):第i阶段投x的最大利润 0≤x≤n,0≤xi≤x
f1(x) = g1(x)
f2(x) = max{g2(x2) + f1(x-x2)}
f3(x) = max{g3(x3) + f2(x-x3)}
②0-1 背包问题
问题
给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。
问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
最优子结构设计
设(y1,y2,…,yn)是所给0-1背包问题的一个最优解。则(y2,…,yn) 是下面相应子问题的一个最优解:
算法设计
设所给0-1背包问题的子问题的最优值为m(i,j)
即m(i,j)是背包容量为j,可选择物品为i,i+1,…,n时0-1背包问题的最优值。
由0-1背包问题的最优子结构性质,可建立计算m(i,j)的递归式如下:
③最长公共子序列问题
问题
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。
令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列i=i0,i1,…,ik-1,使得对所有的j=0,1,…,k-1,有xi=yj。
例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列
算法设计
①递推关系分析
设 A=“a0,a1,…,am-1”, B=“b0,b1,…,bn-1”, Z=“z0,z1,…,zk-1” 为它们的最长公共子序列
有以下结论:
1)如果am-1=bn-1, 且zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列
2)如果am-1≠bn-1,且zk-1≠am-1,则“z0,z1,…,zk-1”是"a0,a1,…,am-2"和"b0,b1,…,bn-1"的一个最长公共子 序列
3)如果am-1≠bn-1,且zk-1≠bn-1,则“z0,z1,…, zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列
由此,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。
因此,该问题具有最优子结构性质。
② 存储、子问题合并
a[i]和b[j]分别存储两个字符串
定义c[i][j]为序列a0,a1,…,ai-1”和“b0,b1,…,bj-1”的最长公共子序列的长度,计算c[i][j]可递归地表述如下:
1)c[i][j]=0 如果i=0或j=0
2)c[i][j]=c[i-1][j-1]+1 如果i,j>0,且a[i-1]=b[j-1]
3)c[i][j]=max(c[i][j-1],c[i-1][j]) 如果i,j>0,且a[i-1]≠b[j-1]
④最大子段和问题
问题
给定n个元素的整数列(可能为负整数)a1,a2,…,an,求形如ai,ai+1,aj,i,j=1,2,…,n,i<=j的子段,使其和为最大。
算法设计
①枚举的时间复杂度O(n^3)
②改进后的枚举时间复杂度O(n^2)
int max_sum2(int n, int a[ ],int *best_i,int *best_j){
int sum=0;
for(int i=1;i<=n;i++){
int this_sum = 0;
for(int j=i;j<=n;j++){
this_sum+=a[j];
if (this_sum>sum){
sum=this_sum;
best_i=i;
best_j=j;
}
}
}
return sum;
}
③动态规划时间复杂度O(n)
划分阶段
分阶段:第i阶段得到前i个数字的最大子段和
选择状态
当前子段的和:this_sum[ i ]
最大子段和:sum[ i ]
int max_sum3(int n, int a[ ]){
int sum=0,this_sum=0;
for(int i=1;i<=n;i++){
if (this_sum>0)
this_sum+=a[i];
else
this_sum=a[i];
if (this_sum>sum)
sum=this_sum;
}
return sum;
}
7.分治
残缺棋盘问题
问题
残缺棋盘是一个有2k×2k (k≥1)个方格的棋盘,其中恰有一个方格残缺。
下图给出k=1时各种可能的残缺棋盘,其中残缺的方格用阴影表示。
这样的棋盘称作“三格板”,残缺棋盘问题就是用这四种三格板覆盖更大的残缺棋盘。
在覆盖中要求:
1)两个三格板不能重叠
2)三格板不能覆盖残缺方格,但必须覆盖其他所有方格
算法设计 (分而治之)
①以k=2时的问题为例,用二分法进行分解得到用双线划分的四个k=1的棋盘。
使用一个①号三格板(图中阴影)覆盖2、3、4号三个子棋盘的各一个方格,把覆盖后的方格,也看作是残缺方格,这时的2、3、4号子问题就是独立且与原问题相似的子问题
k=2其它情况也同理
②推广至k=1,2,3,4,….
将2k×2k棋盘分割为4个2k-1×2k-1 子棋盘(a)所示。
特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。
为将这3个无特殊方格的子棋盘转化为特殊棋盘,可用一个三格板覆盖这3个子棋盘的会合处,如 (b)所示,从而将原问题转化为4个较小规模的棋盘覆盖问题。
③ 递归地使用这种分割,直至棋盘简化为2×2的大小,以方便解决
8.回溯
①n 皇后问题
问题
要在n*n的国际象棋棋盘中放n个皇后,使任意两个皇后都不能互相吃掉。
规则:皇后能吃掉同一行、同一列、同一对角线的任意棋子。
求所有的解。
算法设计
①设n个皇后为xi,分别在第i行(i=1,2,…,n)
②问题的解状态:可以用(1,x1),(2,x2),……,(n,xn)表示n个皇后的位置
由于行号固定,可简单记为:(x1,x2,…,xn);
③ 问题的解空间:(x1,x2,…,xn),1≤xi≤n(i=1,2,…,n),共n^n个状态
④ 约束条件:n个(1,x1),(2,x2) ,… ,(n,xn)不在同一行、同一列和同一对角线上
原问题即:在解空间中寻找符合约束条件的解状态
//非递归回溯框架
int a[n],i;
初始化数组a[];
i=1;
While (i>0(有路可走) and (未达到目标)){
if (i=n)
//搜索到一个解,输出;
else{
a[i]第一个可能的值;
while (a[i]不满足约束条件且在搜索空间内)
a[i]下一个可能的值;
if (a[i]在搜索空间内){
{标识占用的资源; i=i+1;} //扩展下一个结点
}else{
清理所占的状态空间;i=i-1; //回溯
}
}
}
//递归算法框架
int a[n];
try(int i){
if(i>n){
输出结果;
}else{
//枚举i所有可能的路径
for(j=下界;j<=上界;j++) {
//满足限界函数和约束条件
if (check(j)=1)){
a[i]=j;
…… //其它操作
try(i+1);
}
回溯前的清理工作(如:a[i]置空值);
}
}
}
②中国象棋马的遍历问题
问题
在n*m的棋盘中,马只能走“日” 字。马从位置(x,y)处出发,把棋盘的每一格都走一次,且只走一次。找出所有路径。
算法设计
①问题1:解的搜索空间?
棋盘的规模是n*m,是指行有n条边,列有m条边。
马在棋盘的点上走,所以搜索空间是整个棋盘上的n*m个点。
用n*m的二维数组记录马行走的过程,初值为0表示未经过。
②问题2:在寻找路径过程中,活结点的扩展规则?
对于棋盘上任意一点A(x,y),有八个扩展方向:
A(x+1,y+2),A(x+2,y+1),A(x+2,y-1),A(x+1,y-2)
A(x-1,y-2),A(x-2,y-1),A(x-2,y+1),A(x-1,y+2)
用数组fx[8]={1,2,2,1,-1,-2,-2,-1}, fy[8]= {2,1,-1,-2,-2,-1,1,2}来模拟马走“日”时下标的变化过程。
③问题3:扩展的约束条件
不出边界
每个点只经过一次。
棋盘点对应的数组元素初值为0,对走过的棋盘点的值置为所走步数,起点存储“1”,终点存储“n*m”。
函数check,检查当前状态是否合理。
④问题4:搜索解空间?
搜索过程是从任一点(x,y)出发,按深度优先的原则,从8个方向中尝试一个可以走的棋盘点,直到走过棋盘上所有n*m个点。用递归算法易实现此过程
int n=5,m=4,dep,i,x,y,count;
int fx[8]={1,2,2,1,-1,-2,-2,-1},fy[8]={2,1,-1,-2,-2,-1,1,2},a[n][m];
main(){
count=0;dep=1;
print('input x,y');
input(x,y);
if (y>n or x>m or x<1 or y<1) {
print('x,y error!');return;
}
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
a[i][j]=0;
a[x][y]=1;
find(x,y,2);
if (count==0)
print(“No answer!”);
else
print(“count=”,count);
}
find(int x,int y,int dep){
int i,xx,yy;
//加上方向增量,形成新的坐标
for (i=1;i<=8;i++) {
xx=x+fx[i];yy=y+fy[i];
//判断新坐标是否出界,是否已走过
if (check(xx,yy)=1){
a[xx,yy]=dep; //走向新的坐标
if (dep=n*m)
output( );
else
find(xx,yy,dep+1); //从新坐标出发,递归下一层
}
}
a[xx,yy]=0; //回溯,恢复未走标志
}
output(){
count=count+1;
print(“换行符”);
print(“count=”,count);
for (x=1;x<=n;i++) {
print(“换行符”);
for (y=1;y<=m;y++)
print(a[x,y]);
}
}
③装载问题
问题
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i重量为wi,且装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。
如果有,找出一种装载方案。
算法设计
①采用下面策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近c1
②解空间:子集树
③可行性约束函数(选择当前元素):
④上界函数(不选择当前元素): 当前载重量cw+剩余集装箱的重量r<=当前最优载重量bestw
public class Loading {
/** 集装箱数 */
static int n;
/** 集装箱重量数组 */
static int[] w;
/** 第一艘轮船的载重量 */
static int c1;
/** 第二艘轮船的载重量 */
static int c2;
/** 当前载重量 */
static int cw;
/** 当前最优载重量 */
static int bestw;
/** 剩余集装箱重量 */
static int r;
/** 当前解 */
static int[] x;
/** 当前最优解 */
static int[] bestx;
public static void main(String[] args) {
// 下标从1开始,所以第一个元素为0
int[] ww = {0, 10, 40, 40};
int cc1 = 50;
int cc2 = 50;
System.out.println(maxLoading(ww, cc1, cc2));
outPut();
}
/** 返回不超过c 的最大子集和 */
public static int maxLoading(int[] ww, int cc1, int cc2) {
n = ww.length - 1;
w = ww;
c1 = cc1;
c2 = cc2;
cw = 0;
bestw = 0;
x = new int[n + 1];
bestx = new int[n + 1];
r = 0;
// r 初始值为全部集装箱总重
for (int i= 1; i <= n; i++) {
r += w[i];
}
// 计算最优载重量
backTrack(1);
return bestw;
}
/** 回溯算法 */
public static void backTrack(int i) {
// 搜索第 i 层节点
if (i > n) {
// 到达叶节点
if (cw > bestw) {
for (int j = 1; j <= n; j++) {
bestx[j] = x[j];
}
bestw = cw;
}
return ;
}
// 搜索子树
r -= w[i];
if (cw + w[i] <= c1) {
// 重量不超过 c
// 搜索左子树
x[i] = 1;
cw += w[i];
backTrack(i + 1);
// 还原
x[i] = 1;
cw -= w[i];
}
// 只在右子树进行上界函数判断是因为其对左子树无影响
// 左子树是选择放,上界函数 = cw(当前重量) + r(剩余重量)
if (cw + r > bestw) {
x[i] = 0;
// 搜素右子树
backTrack(i + 1);
}
r += w[i];
}
static void outPut() {
int weight = 0;
for (int i = 1; i <= n; i++) {
if (bestx[i] == 0) {
// 第一艘轮船装完后的剩余重量
weight += w[i];
}
}
if (weight > c2) {
System.out.println("不能装入 ");
} else {
System.out.print("轮船一装入的货物为: ");
for (int i = 1; i <= n; i++) {
if (bestx[i] == 1) {
System.out.print(i + " ");
}
}
System.out.println();
System.out.print("轮船二装入的货物为: ");
for (int i = 1; i <= n; i++) {
if (bestx[i] != 1) {
System.out.print(i + " ");
}
}
}
}
}
④素数环问题
问题
把从1到20这20个数摆成一个环,要求相邻两个数的和是一个素数。
分析
搜索从1开始,每个空位有2~20共19种可能
填进去的数合法:与前面的数不相同;与左边相邻的数的和是一个素数
第20个数还要判断和第1个数的和是否素数。
算法设计
①数据初始化
②递归地填数:判断第i种可能是否合法?
A、如果合法:填数;判断是否到达目标(20个已填完):是,打印结果;不是,递归填下一个
B、如果不合法:选择下一种可能
main(){
int a[20],k;
for (k=1;k<=20;k++)
a[k]=0;
a[1]=1;
try(2);
}
try(int i){
int k;
for (k=2;k<=20;k++){
if (check1(k,i)=1 and check3(k,i)=1){
a[i]=k;
if (i=20) {
output();
}else {
try(i+1);
a[i]=0;
}
}
}
}
//判断是否重复
check1(int j,int i){
int k;
for (k=1;k<=i-1;k++)
if (a[k]=j) return(0);
return 1;
}
//判断x是否为素数
check2(int x){
int k,n;
n= sqrt(x);
for (k=2;k<=n;k++)
if (x mod k=0) return 0;
return 1;
}
check3(int j,int i){
if (i<20)
return(check2(j+a[i-1]));
else
return(check2(j+a[i-1]) and check2(j+a[1]));
}
output(){
int k;
for (k=1;k<=20;k++)
print(a[k]);
print(“换行符”);
}
9.图搜索
深度优先与广度优先的策略比较
区别在于扩展结点的过程;
广度优先搜索:
扩展E-结点的所有邻接点,E-结点就成为一个死结点。
一个结点只有一次成为“活结点”。
深度优先搜索:
扩展的是E-结点的邻接结点中的一个,并将其作为新的E-结点继续扩展;
当前E-结点仍为活结点,待搜索完其子结点后,回溯到该结点扩展它的其它未搜索的邻接结点。
一个结点可能多次成为“活结点”。
①迷宫问题
问题
迷宫是许多小方格构成的矩形,在每个小方格中有的是墙(图中的“1”),有的是路(图中的“0”)。走迷宫就是从一个小方格沿上、下、左、右四个方向到邻近的方格,当然不能穿墙。设迷宫的入口是在左上角(1,1),出口是右下角(8,8)。根据给定的迷宫,找出一条从入口到出口的路径。
算法设计(BFS)
从入口开始出发,广度优先搜索所有可到达的方格入队,再扩展队首的方格,直到搜索到出口时,便得到一条通路。
①问题1:如何用所学过的知识来表示现实中的迷宫?
邻接矩阵
利用原有的迷宫数据,检查两点之间是否存在边相连,这样就不必查询任何其他的存储结构了
②问题2:在寻找路径过程中,活结点的扩展?
对于迷宫中任意一点A(X,Y),有四个扩展方向:
向上A(X–1,Y+0)
向下A(X+1,Y+0)
向左A(X+0,Y–1)
向右A(X+0,Y+1)
当对应方格值为0,就扩展为活结点。
为了构造循环体,用数组fx[ ]={-1,1 0,0},fy[ ]={0,0,-1,1}模拟上下左右搜索时下标的变化过程。
函数check,检查当前状态是否合理。
③问题3:在寻找路径过程中,如何实现所遇到的寻找策略和返回策略的解决?
队的实现:数组
队中结点有三个成员:行号、列号、前一个方格在队列中的下标。
struct {int x,y,pre;} sq[100];
④ 问题4:在搜索路径过程中,对搜索过的路径如何标记?
可以另开辟visited[ ][ ]数组记录已搜索过的路径。
也可以用迷宫原有的存储空间置元素值为“-1”时,标识已经访问过该方格。
int maze[8][8]={{0,0,0,0,0,0,0,0},{0,1,1,1,1,0,1,0}, {0,0,0,0,1,0,1, 0},{0,1,0,0,0,0,1,0},{0,1,0,1,1,0,1,0},{0,1,0,0,0,0,1,1},{0,1,0,0,1,0,0,0},{0,1,1,1,1,1,1,0}};
int fx[4]={-1,1,0,0},fy[4]={0,0,-1,1};
struct {
int x,y,pre; //pre用于倒推路径
} sq[100];
int qh,qe,i,j,k;
main() {
search();
}
search(){
qh=0;qe=1;
maze[1][1]=-1; //代表访问过
sq[1].pre=0;
sq[1].x=1;sq[1].y=1;
//当队不空
while(qh<>qe){
qh=qh+1; //出队
//搜索可达的方格
for(k=1;k<=4,k=k+1){
i=sq[qh].x+fx[k];j=sq[qh].y+fy[k];
if (check(i,j)=1){
qe=qe+1; //入队
sq[qe].x=i;sq[qe].y=j;
sq[qe].pre=qh;
maze[i][j]=-1;
if (sq[qe].x=8 and sq[qe].y=8){
out( );return;
}
}
}
print(“No avaliable way.”);
}
check(){
int flag=1;
if (i<1 or i>8 or j<1 or j>8) flag=0; //是否在迷宫内
if(maze[i][j]=1 or maze[i][j]=-1) flag=0; //是否可行
return(flag);
}
//输出路径
out() {
print(“(”sq[qe].x,“,”,sq[qe].y,“)”);
while(sq[qe].pre<>0){
qe=sq[qe].pre;
print(‘--’,“,”,sq[qe].x,“,”,sq[qe].y,“)”);
}
}
算法设计(DFS)
①从始点出发,按深度优先搜索方式搜索该图,一直向着可通行的下一个方格行进,直到搜索到终点时,便得到一条通路。若行不通时,则返回上一个方格,继续搜索其他方向。
②数据结构设计:
用迷宫本身的存储空间除了记录走过的信息,还要标识是否可行:
maze[i][j]=3 标识走过的方格
maze[i][j]=2 标识走入死胡同的方格
当一个方格四个方向都搜索完还没有走到出口(存储为2) ,说明该方格或无路可走或只能走入了“死胡同”。最后存储为“3”的方格为可行的方格。
函数check,检查当前状态是否合理。
注意
用广度优先策略,搜索出的是一条最短的路径,而用深度优先搜索则只能找出一条可行的路径,而不能保证是最短的路径。
②图的着色问题
问题
有如下图所示的七巧板,试设计算法,使用至多4种不同颜色对七巧板进行涂色(每块涂一种颜色),要求相邻区域的颜色互不相同,打印输出所有可能的涂色方案。
算法设计
考虑:能否将七巧板转化为平面图?如何转化?
为了让算法能识别不同区域间的相邻关系,把七巧板上每一个区域看成一个顶点,若两个区域相邻,则相应的顶点间用一条边相连,这样该问题就转化为一个图的搜索问题。
①按顺序分别对1号,2号,......,7号区域进行试探性涂色,用1,2,3,4号代表4种颜色。
涂色过程如下:
1)对某一区域涂上与其相邻区域不同的颜色。
2)若使用4种颜色进行涂色均不能满足要求,则回溯一步,更改前一区域的颜色。
3)转1)继续涂色,直到全部区域全部涂色为止,输出。
②区域之间邻接关系:邻接矩阵data[][]存储
涂色方案:数组color[]存储