《实用算法的分析与程序设计》Chapt 1 基础算法
预备知识:
------------------------------------------------------------------------------
对ACM竞赛的算法大概分了一下类,分成了数学、数据结构和算法三大块。
一 数学(Mathematics)
1 离散数学(Discrete Mathematics)
1.1 图论(Graph Theory)
图的遍历(Graph Traversal): DFS, BFS
最小生成树(Minimum Spanning Tree): Prim, Kruskal
最短路径(Shortest Path): Dijkstra, Floyd
传递闭包(Transitive Closure)
关节点(Articulation Point - UndiGraph)
拓扑排序(Topological Sort - AOV-Network)
关键路径(Critical Path - AOE-Network)
回路问题: 欧拉路(Euler Path), 汉密尔顿回路(Hamilton Tour)
差分约束(Difference Constraints): Bellman-Ford
二部图匹配(Bipartite Matching)
网络流(Network Flow)
...
1.2 组合数学(Combinatorics)
2 数论(Number Theory)
2.1 素数: GCD, LCM...
2.2 同余
3 计算几何(Computational Geometry)
线段相交, 多边形面积, 内点外点的判断, 凸包(Convex Hull), 重心(Bary Center)...
4 线性代数
矩阵(Matrix), 线性方程组(Linear Equations)...
5 概率论
6 初等数学与解析几何
7 高等数学
点积(Dot Product), 差积(Cross Product), 积分(Integral), 微分(Differential)...
二 数据结构(Data Structure)
1 线性结构
线性表(Linear List)
栈(Stack), 队列(Queue)
数组(Array), 串(String), 广义表(General List)
2 非线性结构
树(Tree)
堆(Heap)
图(Graph)
3 排序
3.1 插入排序
直接插入排序(Insert Sort) O(n^2)
折半插入排序(Binary Insert Sort)
希尔排序(Shell Sort)
3.2 交换排序
冒泡排序(Bubble Sort) O(n^2)
快速排序(Quick Sort)?? O(nlogn)
3.3 选择排序
直接选择排序(Select Sort) O(n^2)
锦标赛排序(Tournament Sort) O(nlogn)
堆排序(Heap Sort) O(nlogn)
3.4 归并排序(Merge Sort) O(nlogn)
3.5 基数排序(Radix Sort) O(d(n+radix))
4 查找
4.1 二分(Binary Search)
4.2 树型
二叉搜索树(Binary Search Tree)
平衡搜索树(AVL Tree)
并查集(Union-Find Set)
4.3 哈希(Hashing)
三 算法(Algorithm)
1 模拟算法
2 搜索算法
2.1 枚举搜索(Enumeration)
2.2 深度优先(Depth First Search)
2.3 广度优先(Breadth First Search)
2.4 启发式搜索(Heuristic Search)
3 以"相似或相同子问题"为核心的算法
3.1 递推
3.2 递归(Recursion)
3.3 贪心法(Greedy)
3.4 动态规划(Dynamic Programming)
----------------------------------------------------------------------
第1章 基础算法
1.1 递推法
这是以"相似或相同子问题"为核心的算法
Fn=g(Fn-1)
分为倒推和顺退
1)倒推:
2)顺推
即由起始条件(边界)出发,通过递推关系推出后项值
例2:实数数列
一个实数数列有N项
ai = ( ai-1 - ai+1)/2 +d (1<i<N) (N<60)
键盘输入N,d, a1, aN, m, 输出am
[解] 先将式子变化:ai+1 = ai-1 - 2ai + 2d
问题主要是由a1和aN求出a2
i=2, a3=a2-2a1+2d
i=3, a4=a3-2a2+2d
....
此问题设计到重复计算问题,用动态规划比较好
#include <iostream>
using namespace std;
struct PQR
{
double m_P;
double m_Q;
double m_R;
};
PQR cal_PQR(int n)
{
double *P = new double[n+1];
double *Q = new double[n+1];
double *R = new double[n+1];
P[1] = 0; P[2] = 1;
Q[1] = 0; Q[2] = 0;
R[1] = 1; R[2] = 0;
for(int i = 3; i < n+1; ++i)
{
P[i] = P[i-2] - 2*P[i-1];
Q[i] = Q[i-2] - 2*Q[i-1] + 2;
R[i] = R[i-2] - 2*R[i-1];
}
PQR thePQR = {P[n], Q[n], R[n]};
return thePQR;
}
double cal_a2(double a1, double an, int n, double d)
{
double Pn, Qn, Rn;
Pn = cal_PQR(n).m_P;
Qn = cal_PQR(n).m_Q;
Rn = cal_PQR(n).m_R;
return (an-Qn*d-Rn*a1)/Pn;
}
double seq(double a1, double an, int n, double d, int m)
{
double a2 = cal_a2(a1, an, n, d);
double Pm, Qm, Rm;
Pm = cal_PQR(m).m_P;
Qm = cal_PQR(m).m_Q;
Rm = cal_PQR(m).m_R;
return Pm*a2+Qm*d+Rm*a1;
}
int main()
{
double a1, an, d;
int n, m;
cout << "Please input a1:";
cin >> a1;
cout << "\nPlease input n:";
cin >> n;
cout << "\nPlease input an:";
cin >> an;
cout << "\nPlease input d:";
cin >> d;
cout << "\nPlease input m:";
cin >> m;
if(m > n)
{
cout << "\nm > n, please input m which must be smaller than " << n << "\n";
cin >> m;
}
cout << "\nm = " << m << ". The a" << m << " = :" << seq(a1, an, n, d, m) << endl;
system("PAUSE");
return 0;
}
2. 贪心算法(Greed)
和顺推法比较相似,也是从问题的某个初始解出发,向给定的目标递推。不同的是,推进的每一步不是依据某一固定的递推式,而是做一个当时看似最佳的贪心选择,不断的将问题实例归纳为更小的相似子问题。
但是,这种局部贪心的选择不一定可以得出全局最优解
例2-1 删数问题
键盘输入一个高精度的正整数N,去掉其中任意S个数字后剩下的数字按原左右次序组成一个新的正整数。
给定N和S,寻找一种方案使得剩下数字组成的新数最小。
输入:N, S (N不超过240位)
输出:去掉数字的位置和新的正整数
[解]每步删除一个数字时,搜寻递减区间(a[i]>a[i+1]),找到则删除区间首数字,未找到则删除此整数最后一个数字
由于整数位数不超过240位,而计算机所能表示的正整数有效位最多为11位十进制(包含符号位)
本题需要用字符串表示整数,例如要表示整数736528,则表示成字符串:"736528"
#include <iostream>
using namespace std;
void del_char(char *& N, int pos)
{
int i;
for(i = pos; i < strlen(N)-1; ++i)
{
N[i] = N[i+1];
}
N[i] = '\0';
}
void del_digit_iterative(char *& N, int S)
{
int i, pos, pass;
for(pass = 0; pass < S; ++pass)
{
for(i = 0; i < strlen(N)-1; ++i)
{
if(N[i] > N[i+1])
{
cout << "delete digit " << N[i] << "\n";
del_char(N, i);
break;
}
}
if(i == strlen(N)-1)
{
cout << "delete digit " << N[i] << "\n";
del_char(N,i);
}
}
cout << endl;
}
void del_digit_recursive(char *& N, int S)
{
if(S == 0)
return;
int i, pos;
//one step solution
for(i = 0; i < strlen(N)-1; ++i)
{
if(N[i] > N[i+1])
{
cout << "delete digit " << N[i] << "\n";
del_char(N, i);
break;
}
}
if(i == strlen(N)-1)
{
cout << "delete digit " << N[i] << "\n";
del_char(N,i);
}
//smaller scale problem
del_digit_greed(N, S-1);
}
int main()
{
char N[]= "736528";//{'7','3','6','5','2','8'};
char *ptr = N;
int S = 3;
//cout << "Please input N:\n";
// cin >> N;
//cout << "Please input S:\n";
// cin >> S;
del_digit_recursive(ptr, S);
for(int i = 0; i < strlen(N); ++i)
{
cout << N[i];
}
cout << endl;
system("PAUSE");
return 0;
}
1.3. 递归法
与递推一样,每一个递归定义都有其边界条件。不同的是,递推由边界条件出发,通过递推式求f(n)的值,从边界到求解的全过程十分清楚;而递归则是从函数自身出发达到边界条件:系统用堆栈把每次调用的中间结果(局部变量和返回地址值)保存起来,直至到达递归边界。然后返回调用函数,中间结果相继出栈恢复。
递归算法的效率往往很低,费时和费内存。但是它表达问题清楚。
递归按其调用方式分:直接递归( 递归过程P直接调用自己)
间接递归(P 包含令一过程,而D又调用P)
适用于:
- 数据以递归的形式定义,如Fibonacci数列
- 数据结构按递归定义,如树的遍历,图的搜索
- 问题解法按递归算法实现,如回溯法等
第1类递归问题可转化为递推算法
第2、3类递归问题,可以利用堆栈结果将其转换为非递归算法
例3-1 划分问题
将集合{s1, s2, s3, s4, ..., sn)划分成k个子集,不能有空集。问有多少种划分方法。
〔解〕设最终有f(n, k)种划分数
- 如果{an}是划分的k个子集的一个,那么把{a1, a2....an-1}划分成k-1个子集有f(n-1, k-1)个划分数
- 如果{an}不是k个子集中的一个,则an必与其它元素构成一个子集。首先把{a1,a2,...,an-1}划分为k个集合,再把an放入k个集合中的一个。一共有k×f(n-1,k}种划分数
上面两种情况互斥,所以最终划分数 f(n, k)=f(n-1, k-1)+k*f(n-1,k)
很显然,问题变成了一个递归问题。
例3-2 背包问题
设有一个背包,可以放入的重量为s. 现有n件物品,重量分别为w1, w2, ..., wn, 并假设wi(1<=i<=)均为正整数,且顺序存放在数字w中(w: array[1...n] of integer).现要求设计一个布尔函数knap(s0, n0), 如果从这n件物品中选择n0件放入此背包,使得放入的重量之和正好为s0,函数返回true,并输出一组被选中的各物品的重量. 否则函数返回false.
[解] 同样分析互斥的2种情况,
- 当选择的物品中不包含wn:knap(s, n) = kanp(s, n-1)
- 当选择的物品中包含wn:knap(s-wn, n-1)
边界条件:
1.6 模拟法
有些自然界和日常生活中的事件,若用计算机很难建立枚举、递归等算法,甚至建立不了数学模型。
[例1.6-1]猜数游戏
人和计算机作猜数游戏。人默想一个4位数,由计算机来猜。计算机将所猜的数显示到屏幕上,并问两个问题:
- 猜对了几个?
- 测对的数字中有几个位置也对?
人通过键盘来回答这2个问题。计算机一次又一次的猜,直到猜对位置。
情形如下:
人默想一个数是5122,计算机第一次猜1166,然后问
然后计算机第二次猜1287,然后问
计算机最后一次猜5122,然后问
则表示猜完了。计算机显示最后猜中的数,并报告共猜了几次
问题1 编程实现这样一个猜数游戏程序,屏幕显示格式为:
第二行显示计算机所猜的四位数
第三行提问猜对的数字个数.用"Number"
第四行提问位置对的数子个数.用"Position";
第五行显示当前已猜对的步数.用"Step xx"
其中末尾数字由键盘输入,最后绐出结束信息
问题2 仍是这样一个游戏.但要求计算机既是猜数者.又要模拟默想这个数的人
(要猜的数由键盘输入),屏暮显示格式为:
第一行显示人所默想的数,用" " xxxx
第二行至第五行同问题1,只不过末尾数字不再由键盘输入,而是计算机判断后自
动显示。
问题3 从文本文件GUESS.DAT中读人20个四位数.一个接一个地让计算机猜,
统计猜中所需的总步数。
*问题分析与算法设计
解决这类问题时,计算机的思考过程不可能象人一样具完备的推理能力,关键在于要将推理和判断的过程变成一种机械的过程,找出相应的规则,否则计算机难以完成推理工作。
基于对问题的分析和理解,将问题进行简化,求解分为两个步聚来完成:
首先确定四位数字的组成,
然后再确定四位数字的排列顺序。可以列出如下规则:
1)分别显示四个1,四个2,......,四个0,确定四位数字的组成。
2)依次产生四位数字的全部排列(依次两两交换全部数字的位置)。
3)根据人输入的正确数字及正确位置的数目,进行分别处理:
(注意此时不出现输入的情况,因为在四个数字已经确定的情况下,若有3个位置正确,则第四个数字的位置必然也是正确的)
若输入4:游戏结束。
判断本次输入与上次输入的差值
若差为2:说明前一次输入的一定为0,本次输入的为2,本次交换的两个数字的位置是正确的,只要交换另外两个没有交换过的数字即可结束游戏。
若差为-2:说明前一次输入的一定为2,本次的一定为0。说明刚交换过的两个数字的位置是错误的,只要将交换的两个数字位置还原,并交换另外两个没有交换过的数字即可结束游戏。
否则:若本次输入的正确位置数<=上次的正确位置数
则恢复上次四位数字的排列,控制转3)
否则:将本次输入的正确位置数作为“上次输入的正确位置数”,控制转3)。
#include <iostream>
#include <vector>
using std::cout;
using std::cin;
using std::vector;
vector<int> v;
int right, position, count=0;
int main()
{
cout << "Now guess your number in mind is # # # #. ";
for(int i =1; i <=9; ++i)
{
cout << "I guess: " << i << " " << i << " " << i << " " << i << "\n";
count++;
cin >> right;// >> position;
if(right > 0)
{
for(int idx=0; idx<right;++idx)
{
v.push_back(i);
}
}
if(v.size() == 4)
break;
}
cout << "now 4 digits is :" << v[0] << "," << v[1] << "," << v[2] << "," << v[3] << ".\n";
for(int i=0; i < 4; ++i)
{
for(int j=0; j < 4; ++j)
{
if(j==i)
{
continue;
}
for(int k=0; k <4; ++k )
{
if(k==i || k==j)
{
continue;
}
for(int l=0; l < 4; ++l)
{
if(l==i || l==j || l==k)
{
continue;
}
cout << "I guess: " << v[i] << " " << v[j] << " " << v[k] << " " << v[l] << "\n";
count++;
cin >> position;/*right >>*/
if(position == 4)
{
cout << "Totally " << count << " pass!";
system("PAUSE");
return 0;
}
}
}
}
}
}