《实用算法的分析与程序设计》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)倒推:

在不知初始条件的情况下,通过递推获知问题的解
example1


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)

适用于:

  1. 数据以递归的形式定义,如Fibonacci数列
  2. 数据结构按递归定义,如树的遍历,图的搜索
  3. 问题解法按递归算法实现,如回溯法等

第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位数,由计算机来猜。计算机将所猜的数显示到屏幕上,并问两个问题:

  1. 猜对了几个?
  2. 测对的数字中有几个位置也对?

人通过键盘来回答这2个问题。计算机一次又一次的猜,直到猜对位置。

情形如下:

人默想一个数是5122,计算机第一次猜1166,然后问

  • 猜对了几个?                                   1
  • 测对的数字中有几个位置也对?       1

    然后计算机第二次猜1287,然后问

  • 猜对了几个?                                   2
  • 测对的数字中有几个位置也对?       0

    计算机最后一次猜5122,然后问

  • 猜对了几个?                                   4
  • 测对的数字中有几个位置也对?       4

    则表示猜完了。计算机显示最后猜中的数,并报告共猜了几次

    问题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;
                        }
                    }
                }
            }
        }
    }
  • posted @ 2007-10-29 16:38  中土  阅读(2296)  评论(1编辑  收藏  举报
    ©2005-2008 Suprasoft Inc., All right reserved.