Download as pdf or txt
Download as pdf or txt
You are on page 1of 49

算法分析与设计

——以大学生程序设计竞赛为例

芦旭
lxuu306@sdau.edu.cn

1
◼ 考试时间:第12周周二(11月21日)下午第一大节
◼ 考试内容:第一章到第六章(回溯算法)
◼ 考试题型:

◼ 实验课:实验报告word文档,总结本学期做过的作业。命
名“班级-学号-姓名”,无具体格式和字数要求。11月19
号之前发给各班班长。请按时提交。
第1章 算法概述
1.1 引言
1.1.1 算法的描述
1.1.2 算法的设计
1.2 算法的复杂性
1.2.1 时间复杂性
1.2.2 空间复杂性
1.3 大学生程序设计竞赛概述
1.4 程序设计在线测试题库
3
3
什么是算法?
◼ 算法(Algorithm)是一系列解决问题的清晰指令,代表着
用系统的方法描述解决问题的策略机制。
◼ 广义地讲,算法是指解决问题或执行任务的方法或过程。
◼ 也可以说,算法是一种有序的操作序列,解决特定问题的
步骤。
◼ 同一个问题可以有不同的算法,不同的算法解决同一个问
题的效率可能千差万别。
算法和程序
◼ 程序,是算法用某种程序设计语言的具体实现。
◼ 算法 不等于 程序。
◼ 算法要用计算机去执行,因此必须使用一种程序设计语言
来把它编成计算机程序,才能让计算机去执行这个算法。
算法设计的先驱者唐纳德.E.克努特
(DONALD E.KNUTH)描述算法的特征
(1)有穷性(Finiteness)
算法在执行有限步之后必须终止。
(2)确定性(Definiteness)
算法的每一步骤必须有确切的定义。要执行的每一个动作都是清晰
的、无歧义的。欧几里德算法规定了m和n都是正整数,从而保证了算法
能够确定地执行。
(3)输入(Input)
一个算法有0个或多个输入,作为算法开始执行前的初始值,或初始
状态。所谓0个输入是指算法本身定出了初始条件。
(4)输出(Output)
一个算法有一个或多个输出,以反映对输入数据加工后的结果。没
有输出的算法是毫无意义的。
(5)可行性(Effectiveness)
6
在有限时间内完成计算过程。
1.2 算法的复杂性
算法的复杂性有时间复杂性和空间复杂性之分。
复杂性与所解决问题的规模(输入数据的大小)有关。
算法的时间复杂性(Time Complexity)是指执行算法所
需要的时间。
算法的空间复杂性(Space Complexity)是指算法需要消
耗的内存空间。

7
二、运行时间的上界(O)

8
二、运行时间的上界

9
三、运行时间的下界Ω

Ω 与O正好相反, Ω(g(n)) 可以理解为:下界为g(n)的所


有函数的集合;等号同样理解为属于;
f(n)的阶不低于(或者说大于等于)g(n)的阶; 10
四、运行时间的准确界Θ

11
第2章 数据结构和标准模板库STL
• 2.1 栈Stack:后进先出(LIFO —Last In First Out )
• 2.2 向量Vector:是一个动态数组,随机存取任何元素都能在常
数时间完成。
• 2.3 映射Map
提供了一种将键(key)与值(value)关联起来的方式。优点
是可以通过键快速查找对应的值,提供了高效的键值对的存储和访问
方式。
Map内部数据的组织是一颗红黑树,这颗树具有对数据自动排序
的功能,所以在Map内部所有的数据Key都是有序的。
• 2.4 列表List:是一个线性链表结构。
• 2.5 集合Set:其中所包含的元素的值是唯一的(集合中不允许重
复的元素),且集合中的元素按一定的顺序排列。
• 2.6 队列Queue:先进先出(FIFO—First In First Out) 12
• 2.7 优先队列Priority Queue:队列中最大的元素总是位于队首,
所以出队时,将当前队列中最大的元素出队。
第3章 递归与分治策略
3.1 递归算法 3.2 分治策略
3.1.1 Fibonacci数列 3.2.1 分治法的基本步骤
3.1.2 集合的全排列问题 3.2.2 分治法的适用条件
3.1.3 整数划分问题 3.2.3 二分搜索技术
3.2.4循环赛日程表
3.2.5 棋盘覆盖问题
3.2.6 选择问题
3.2.7输油管道问题
3.2.8 半数集问题
3.2.9 整数因子分解
3.2.10取余运算
3.3 Big String 13
3.2 分治策略
分治策略是对于一个规模为n的问题,若该问题可以
容易地解决(比如说规模n较小)则直接解决,否则
将其分解为k个规模较小的子问题,这些子问题互相
独立且与原问题形式相同。
递归地解这些子问题,然后将各子问题的解合并得
到原问题的解。

14
3.2.1 分治法的基本步骤
分治法在每一层递归上都有三个步骤:
1. 分解:将原问题分解为若干个规模较小,相互独立,与
原问题形式相同的子问题;
2. 解决:若子问题规模较小而容易被解决则直接解,否则
递归地解各个子问题;
3. 合并:将各个子问题的解合并为原问题的解。

15
3.1.1 FIBONACCI数列
1 (n = 0,1)
F ( n) = 
F(n-1 ) + F(n-2 ) (n  1)

算法3.1 Fibonacci数列的递归算法
int fib(int n) //声明一个函数fib,它接受一个整数参数n,并返回一个整数。
{
if (n<=1) return 1; //判断n的值是否小于等于1。如果n小于等于1,则
返回1。这是递归算法的终止条件,当n为0或1时,Fibonacci数列的第n个数为1。
return fib(n-1)+fib(n-2); //如果n大于1,那么函数将调用自身两次,
并将两次调用的结果相加。第一次调用是fib(n-1),它返回Fibonacci数列的第n-1
个数;第二次调用是fib(n-2),它返回Fibonacci数列的第n-2个数。然后将这两个
数相加,得到Fibonacci数列的第n个数。
}
该算法的效率非常低,因为重复递归的次数太多。 16
3.1.1 FIBONACCI数列
1 (n = 0,1)
F ( n) = 
F(n-1 ) + F(n-2 ) (n  1)

算法3.2 Fibonacci数列的递推算法
int fib[50]; //声明了一个名为 fib的整型数组,大小为 50。这个数组用于保存斐波那契数
列的中间结果.
void fibonacci(int n) //定义了一个名为 fibonacci 的函数,它接受一个整数参数 n
{
fib[0] = 1; //将数组的第一个元素设置为 1
fib[1] = 1; //将数组的第二个元素设置为 1
for (int i=2; i<=n; i++) //使用一个循环来计算斐波那契数列的其他元素,循环
的起始条件是 i=2
fib[i] = fib[i-1]+fib[i-2]; //将数组中第 i 个元素的值设置为前两个
元素的和 17

}
3.2.3 二分搜索技术
给定n个元素a[0:n-1],需要在这n个元素中找出一个
特定元素x。
⚫ 首先对n个元素进行排序,可以使用C++标准模板库函数sort()。
⚫ 二分搜索技术充分利用了n个元素已排好序的条件,采用分治策
略的思想,在最坏情况下用O(log n) 时间完成搜索任务。
二分搜索算法的基本思想是将n个元素分成个数大致相同
的两半,取a[n/2]与x作比较。
⚫ 如果x=a[n/2],则找到x,算法终止。
⚫ 如果x<a[n/2],则我们只要在数组a的左半部分继续搜索x。
⚫ 如果x>a[n/2],则我们只要在数组a的右半部分继续搜索x。

0 1 2 3 4 5 6 7 8 9 10
7 14 17 21 27 31 38 42 46 53 75
18

left middle right


算法3.6 二分搜索算法
//数组a[]中有n个元素,已经按升序排序,待查找的元素x
template<class Type>
int BinarySearch(Type a[],const Type& x,int n) //声明函数
BinarySearch,接受三个参数:数组a,要查找的元素x,以及数组的大小n
{
int left=0; //左边界
int right=n-1; //右边界
while(left<=right) //递归边界,左边界小于等于右边界
{
int middle=(left+right)/2; //中点
if (x==a[middle]) return middle;
if (x>a[middle]) left=middle+1;
else right=middle-1;
}
return -1; //如果循环结束后仍然没有找到目标元素
19
}
第4章 动态规划
4.1 矩阵连乘积问题 4.3 最长公共子序列
4.1.1 分析最优解的结构 4.3.1 最长公共子序列的结构
4.1.2 建立递归关系 4.3.2 子问题的递归结构
4.1.3 计算最优值 4.3.3 计算最优值
4.1.4 构造最优解 4.3.4 构造最长公共子序列
4.2 动态规划算法的基本要素 4.4 最大子段和
4.1.1最优子结构 4.5 0-1背包问题
4.1.2重叠子问题 4.5.1 递归关系分析
4.1.3 备忘录方法 4.5.2 算法实现
4.6 最长单调递增子序列
4.7 数字三角形问题
20
一、动态规划的基本思想
动态规划算法(Dynamic Programming)通常用于求解具
有某种最优性质的问题。
基本思想:是将待求解问题分解成若干个不独立的子问题,
先求解子问题,将子问题的答案记录在表格中(当需要再次
求解这个子问题时,可以通过查表得到该子问题的解,而不
需要重复求解。这样就可以避免大量的重复计算,节省时间
),最后从这些子问题的解得到原问题的解。

21
三、设计动态规划法的步骤
1. 找出最优解的性质(一个最优策略的子策略总是最优的)
,并刻画其结构特征;
2. 递归地定义最优值(写出动态规划方程/递推公式);
3. 以自底向上的方式计算出最优值;
4. 根据计算最优值时得到的信息,构造一个最优解。
⚫ 步骤1~3是动态规划算法的基本步骤。

22
四、动态规划问题的特征
动态规划算法的有效性依赖于问题本身所具有的两个重要性
质:
1. 最优子结构:
⚫ 当问题的最优解包含了其子问题的最优解时,称该问题具有最优子
结构性质。
2. 重叠子问题:
⚫ 在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问
题,有些子问题被反复计算多次。动态规划算法正是利用了这种子
问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一
个表格中,在以后尽可能多地利用这些子问题的解。

23
4.5 0-1背包问题
给定一个物品集合s={1,2,3,…,n},物品i的重量
是wi,其价值是vi,背包的容量为W,即最大载重量不超过
W。在限定的总重量W内,我们如何选择物品,才能使得物
品的总价值最大。
⚫ 如果物品不能被分割,即物品i要么整个地选取,要么不选取;
⚫ 不能将物品i装入背包多次,也不能只装入部分物品i,则该问题称
为0—1背包问题。
⚫ 如果物品可以拆分,则问题称为背包问题,适合使用贪心算法。

24
4.5 0-1背包问题
假设xi表示物品i装入背包的情况,xi=0,1。
⚫ 当xi=0时,表示物品没有装入背包;
⚫ 当xi=1时,表示把物品装入背包。
n
约束方程: wi xi ≤W
i =1
n
目标函数: max  vi xi
i =1

因此问题就归结为找到一个满足上述约束方程,
并使目标函数达到最大的解向量:
X={x1,x2,…,xn},

25
4.5 0-1背包问题
先定义状态:f(k, w):背包容量为w,现有k件物品可选,
求所能装进背包的最大价值。

26
4.5 0-1背包问题
再给出递推公式(状态转移方程):
f(k,w)= f(k-1, w), wk>w (太重,放不下)
max(f(k-1,w), f(k-1,w-wk)+vk), wk<=w
第k件不放进包 第k件放进包
重量和价值都不变 包内重量应减少
价值应增加

27
4.5 0-1背包问题
填写动态规划表(备忘录):
背包容量

物品
编号

28
#include<iostream>
using namespace std;
const int maxm = 201, maxn = 31;
int m, n; \\分别表示背包容量和物品数量
int w[maxn], c[maxn]; \\分别用于存储每个物品的重量和价值
int f[maxn][maxm]; \\用于存储最优价值
int max(int x,int y) { x>y?x:y;} //求x和y最大值
int main(){
cin>>m>>n; //背包容量m和物品数量n
for (int i = 1; i <= n; i++)
cin>>w[i]>>c[i]; //读取每个物品的重量和价值
for (int i = 1; i <= n; i++) //遍历物品
for (int v = m; v >= 1; v--) //遍历背包容量从m到1的所有可能取值
if (w[i] <= v)
f[i][v] = max(f[i-1][v],f[i-1][v-w[i]]+c[i]); //f[i][v]表示前i
件物品总重量不超过v时的最优价值
else
f[i][v] = f[i-1][v];
cout<<f[n][m]; // f[n][m]为最优解
return 0;
}
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

31
第5章 贪心算法
➢ 贪心算法总是作出在当前看来最好的选择;并且每次贪
心选择都能将问题化简为一个更小的与原问题具有相同
形式的子问题。
➢ 贪心算法并不从整体最优考虑,它所作出的选择只是在
某种意义上的局部最优选择。当然,希望贪心算法得到
的最终结果也是整体最优的。
➢ 在一些情况下,即使贪心算法不能得到整体最优解,其
32
最终结果却是最优解的一个很好近似。
第5章 贪心算法
整体最优与局部最优:
现有面值分别为1角1分,5分,1分的硬币,请给出找1角5
分钱的最佳方案。
➢ 贪心(选择当前最大面值的硬币)(5枚):
15-11=4
4-4*1=0
➢ 最佳(3枚)
3*5
33
5.2 贪心算法的理论基础

➢ 利用贪心策略解题,需要解决以下两个问题:
① 该题是否适合于用贪心策略求解(贪心选择性质+最
优子结构性质)【是否满足性质】
② 如何选择贪心标准,以得到问题的最优/较优解【如
何选择策略】
5.2.1 贪心选择性质
贪心选择性质是指所求问题的整体最优解可以通过一系
列局部最优的选择,即贪心选择来达到。

35
5.2.2 最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题
具有最优子结构性质。

贪心算法的每一次操作都对结果产生直接影响,而动态规
划则不是。
⚫ 贪心算法对每个子问题的解决方案都做出选择,不能回退;
⚫ 动态规划则会根据以前的选择结果对当前进行选择,有回退功能。
⚫ 动态规划主要运用于二维或三维问题,而贪心一般是一维问题。

36
5.3 背包问题
给定一个载重量为M的背包,考虑n个物品,其中第i个
物品的重量 ,价值wi (1≤i≤n),要求把物品装满背
包,且使背包内的物品价值最大。
有两类背包问题(根据物品是否可以分割),如果物品
不可以分割,称为0—1背包问题(动态规划);如果物
品可以分割,则称为背包问题(贪心算法)。

37
5.3 背包问题
有3种方法来选取物品:
(1)当作0—1背包问题,用动态规划算法,获得最优值220;
(2)当作0—1背包问题,用贪心算法,按性价比从高到底顺
序选取物品,获得最优值160。由于物品不可分割,剩下的空
间白白浪费。
(3)当作背包问题,用贪心算法,按性价比从高到底的顺序选
取物品,获得最优值240。由于物品可以分割,剩下的空间装
入物品3的一部分,而获得了更好的性能。
0—1背包 0—1背包 背包问题
动态规划 贪心算法 贪心算法

20 100 20 20 80
n 1 2 3
+ +
重量 10 20 50 30
20 100 20 100
价值 60 100
30 120 30 120 38
+ +
性价比 6 20 5 4
10 10 60 10 60
60 100 120 背包 =220 =160 =240
算法5.3 计算背包问题的贪心算法
//形参n是物品的数量,c是背包的容量M,数组a是按物品的性价比降序排序
double knapsack(int n, bag a[], double c)
{
double cleft = c; //背包的剩余容量
int i = 0;
double b = 0; //获得的价值
//当背包还能完全装入物品i
while(i<n && a[i].w<cleft)
{
cleft -= a[i].w;
b += a[i].v;
i++;
}
//装满背包的剩余空间
if (i<n) b += 1.0*a[i].v*cleft/a[i].w;
return b;
} 39
第6章 回溯算法
6.1 回溯算法的理论基础
6.1.1 问题的解空间
6.1.2 回溯法的基本思想
6.1.3 子集树与排列树
6.2 装载问题
6.3 0-1背包问题
6.4 图的m着色问题
6.5 n皇后问题
6.6 旅行商问题
6.7 流水作业调度问题 40

6.8 子集和问题
第6章 回溯算法
回溯法是一种组织搜索的一般技术,用它可以系统的搜
索一个问题的所有解或任一解。

41
6.1.1 问题的解空间
应用回溯法求解时,需要明确定义问题的解空间。
一个问题可能的所有解构成解空间。
在确定了解空间的组织结构后,回溯从开始结点(根结点)
出发,以深度优先的方式搜索整个解空间。
6.1.2 回溯法的基本思想
在回溯法搜索解空间树时,通常采用剪枝操作避免无效搜索,
以提高回溯法的搜索效率:
1. 用约束函数在扩展结点处减去不满足约束条件的子树;
2. 用限界函数减去不能得到最优解的子树。
⚫ 解0—1背包问题的回溯法用剪枝函数剪去导致不可行解的子
树。
⚫ 解旅行商问题的回溯算法中,如果从根结点到当前扩展结点
的部分周游路线的费用已超过当前找到的最好周游路线费用
,则以该结点为根的子树中不包括最优解,就可以剪枝。

43
【例1】素数环:从1到20这20个数摆成一个环
,要求相邻的两个数的和是一个素数。
➢分析:
◆从1开始,每个空位有20种可能,只要填进去的
数合法:
① 与前面已经填过的数不相同;
② 与左边相邻的数的和是一个素数。
③ 第20个数还要判断和第1个数的和是否素数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
例1 素数环
➢算法流程:
1、数据初始化(布尔数组,标记某个数是否已经填入到素数
环中);
2、递归填数:判断第i个数填入是否合法;
A、如果合法(剪枝条件):
填数;判断是否到达目标(20个已填完):
是,打印结果;
不是,递归填下一个;
B、如果不合法:选择下一种可能;
子集树解决方案
#include<iostream>
#include<cstdlib>
#include<cmath>
using namespace std;
bool b[21]={0}; //判断 i 是否出现在素数环中
int total=0,a[21]={0}; //a记录素数环中的每一个数
void search(int t); //回溯过程,形参t表示当前在素数环中的数的编号
void print(); //输出方案
bool pd(int,int); //判断两数之和是不是素数
void search(int t){ //寻找所有解
int i;
for (i=1;i<=20;i++) //遍历20个数
if (pd(a[t-1],i)&&(!b[i])){ //判断当前数字与前一个数字的和是否是
素数,并且检查当前数字是否已经在素数环中
a[t]=i; //将当前数字放入素数环
b[i]=1; //解向量设为真
if (t==20) { //表示找到一个解,使用 pd 函数检查首尾两个数字的
和是否是素数,如果是素数,输出这个解
if (pd(a[20],a[1])) print(); }
else
search(t+1); //否则,递归寻找下一个数字
b[i]=0;
}
}
int main(){
search(1);
cout<<total<<endl; //输出总方案数
return 0;
}

int print(){
total++;
cout<<"<"<<total<<">";
for (int j=1;j<=20;j++)
cout<<a[j]<<" ";
cout<<endl;
}
bool pd(int x,int y){
int k=2,i=x+y;
while (k<=sqrt(i)&&i%k!=0) k++;
if (k>sqrt(i)) return 1;
else return 0;
}

子集树解决方案:从数字1到20中选择一个数字放入素数
环的当前位置,然后继续递归地搜索下一个位置的数字,以
此方式构建素数环。这个方法类似于在每一步中选择一个数
字放入集合(子集),然后继续递归地搜索,直到找到有效
的解或者不再满足条件,这就构成了一个子集树。

You might also like