首页 > 代码库 > 算法设计手册(第2版)读书笔记, Springer - The Algorithm Design Manual, 2ed Steven S.Skiena 2008

算法设计手册(第2版)读书笔记, Springer - The Algorithm Design Manual, 2ed Steven S.Skiena 2008

The Algorithm Design Manual, 2ed

跳转至: 导航、 搜索

Springer - The Algorithm Design Manual, 2ed Steven S.Skiena 2008

目录

  • 1介绍
  • 2算法设计
  • 3数据结构
  • 4排序和搜索
  • 5图遍历
  • 6加权图
  • 7组合搜索与启发式
  • 8DP
  • 9Intractable问题与近似算法
  • 10怎样设计算法
  • 11数据结构
  • 12数值问题
  • 13组合问题
  • 14图问题:P
  • 15图问题:困难的
  • 16计算几何
  • 17集合与字符串
  • 18算法资源

介绍

  1. 求一维最长不重叠的子区间的集合:从左往右,总是选择结束时间最早的(启发式=〉贪心策略)
    1. 一般的问题:贪心策略的正确性如何证明?比如MST(好像忘掉了)
  2. War Story:Psychic Modeling(这个问题到底是什么?)
    1. 一开始建模为set cover,但是不对(确保winning pair不需要cover all,正确的怎么做作者没有说明)

算法设计

  1. p48 快速指数幂
  2. War Story:金字塔传奇(算法的并行加速)
    1. Waring‘s问题:任何整数可表示为4个整数的平方和
    2. (猜想)任何整数可表示为<=5个金字塔数(形如(m^3-m)/6)的和(其实就是元素满足特定约束的背包问题)
      1. 首先按大小排序,然后构造2个元素和的所有情形(仍然按大小排序)
        1. 评论:这里构造预计算集相当于空间换时间,而要求数据有序则使得可以二分搜索
      2. (层次递进)对数k,首先确认它是否是1818个最小的金字塔数p之一,再确认它是否是2个金字塔数的和two之一(二分搜索!)
        1. 要确认是否是3个金字塔数的和之一,这时需要分治:首先从k中减去p[i];复杂度O(n^(1/3) lgn)
        2. 4个情况,分为‘2+2’,即k-two[i]
      3. 总复杂度O(n^(4/3) lgn),相比原来的穷尽搜索(O(n^2))
  3. Esoteric函数
    1. 反向Ackerman:invA(n)‘缓增’
    2. loglog n
    3. logn / loglog n(n个叶子节点的二叉树的高度)
    4. log^2 n

数据结构

  1. Contiguous vs Linked
  2. 栈与队列
  3. 字典
  4. BST
    1. 平衡搜索树
  5. 优先队列
  6. War Story:Stripping Triangulations(把3D模型的三角剖分曲面数据处理为条带)
    1. 把每个三角形映射为图的顶点
    2. 注意,所谓的‘strip’就是指2个三角形共享一条边,以‘边共享’作为图的顶点邻接关系
    3. 则本优化问题相当于求图的最短Hamiltonian路径,NP-complete
    4. 启发式:to find the largest strip to peel off next
  7. 哈希与字符串
    1. 冲突消解:chaining / open-addressing?(实际上开地址法遇到桶溢出是很不划算的,但chaining有一般的链表维护开销)
    2. 字符串匹配的Rabin-Karp算法:如果2个字符串相同,则它们的长度和hash应该相等
      1. 注意,这里源字符串的hash计算是‘滚动式’的——‘滑动窗口hash’(要求hash函数基于线性加权取模)
        1. 取模为某素数M,则hash碰撞的可能性为1/M=〉随机算法?
  8. Specialized DS
  9. War Story:String ‘em Up
    1. DNA序列:(A, C, T, G)*
    2. sequencing by hybridization(SBH):确信k-子串集,构造可能的2k-子串集,如AC, CA, CC => ACCA可能,CAAC不可能
      1. BST --> hashtable -> 后缀树 -> 压缩后缀树 ->

排序和搜索

  1. 排序的应用:求点集的凸包(Convex Hull)
    1. 按x从左到右排序,然后依次插入目标凸包集合(维护一个上下高度范围约束?这里似乎没讲清楚)
  2. HeapSort
    1. 可以视为插入排序的变形:pg_insert -> bubble_up(当然,如果反方向的话可以bubble_down)
    2. 用最小堆来排序:extract_min,这时堆顶出现了空洞,把最后一个(right-most)元素补进来,bubble_down
    3. 注意,这里初始构造堆可以bubble_up也可以bubble_down,但中间过程heap_adjust总是bubble_down
      1. 考虑到如果使用最小堆排序的话,每次取出的当前最小元素放哪里?这样看来还是用最大堆排序更合适点,作者的代码:
        heapsort(item_type s[], int n){//这里使用了额外的空间,如果用最大堆就可以in-place排序了
        int i;
        priority_queue q;
        make_heap(&q, s, n);
        for(i=0; i<n; ++i) s[i] = extract_min(&q);
        }
    4. 更快的建堆:for(int i=q->n; i>=1; --i) bubble_down(q,i); //搞不明白作者这里的priority_queue内部元素下标为什么从1开始?
    5. Stop and Think:如何O(k)判断堆中第k小的元素是否>=指定x?=> 转化为在层次1..k范围内统计<x的元素计数!
      1. 低效的做法:extract_min k次以得到kth smallest;
        1. 第k小的元素的层次不可能大于k:在此范围内线性扫描?
      2. 递归的compare_heap:传入当前已经>=x的计数count,返回修改累计后的新count
        1. 结束条件:if( count<=0 || i> q->n) return count;
        2. 注意,这里count概念的特殊传参是一个技巧
  3. War Story:Give me a Ticket on an Airplane(最便宜的飞机票路径规划?)
    1. X+Y排序(X,Y是有序列表,求所有X+Y的有序列表)
    2. 即时的增量计算结果 => 如果要全局最优,则需要维护一个理论上没上限的缓存(考虑到重复值的情况)
    3. 问题是实际情况并不满足这种简单的约束!
  4. MergeSort
    1. 这里作者再次使用了额外的queue
  5. QuickSort(注意理解这里的partition算法:‘滚动的>=区间’)
    int partition(item_type s[], int l, int h){
    int i; /* counter */
    int p; /* pivot element index */
    int firsthigh; /* divider position for pivot element */
    p = h;
    firsthigh = l;
    for (i=l; i<h; i++)
    if (s[i] < s[p]) {
    swap(&s[i],&s[firsthigh]);
    firsthigh ++;
            }
    swap(&s[p],&s[firsthigh]);
    return(firsthigh);
    }
  6. 桶排序
  7. War Story:Skiena for the Defense
    1. 外排序:多路归并
  8. 二分查找与相关算法
    1. O(lgn)统计相同元素的出现次数:BS去除if(A[middle]==key)return middle;一句,分别用<和>=作为comparator,得到上下界
      1. 前提是元素已经有序?嗯,不是很稀奇
    2. 单侧二分查找:同样不稀奇
  9. 分治
    1. master定理:T(n)=aT(n/b)+f(n),注意loga/logb与f(n)次数的关系

图遍历

  1. 2种表示:邻接矩阵、邻接表
  2. War Story:I was a Victim of Moore‘s Law
    1. To make a program run faster, just wait(哈哈)
  3. War Story:Getting the Graph(注意数据集的内在关联性)
  4. 顶点状态:
    1. undiscovered
    2. discovered(已访问过,但它的incident边没有全部处理)
    3. processed
  5. 3个回调时机:
    1. process_vertex_early(v)
    2. process_edge(v, w)
    3. process_vertex_late(v)
  6. BFS
    1. 例子:判断是否可顶点2-着色(即是否是二部图), process_edge(v, w): //这里color[v]默认着色第一种
      1. if( color[v]==color[w]) bipartite=FALSE;
      2. color[w] = complement( color[w] );
  7. 无向图上的DFS
    1. 边的区分:树边(parent->child)、回边(child->parent)、cross边(到非parent节点的边)
    2. 额外的entry_time[v]和exit_time[v]?
    3. 无向图找圈:if( parent[v]!=w )(回边,if写法有点怪异) //注意,在dfs框架里,parent[w]==v 或 ‘w已访问过但未处理’
    4. Articulation顶点(割点?去除会导致图不再弱连通)
      1. reachable_ancestor[v] v的最早(?)可达祖先,初始reachable_ancestor[v]=v
      2. 核心判断条件:if( class(v,y)==BACK_EDGE && parent[v]!=w ) //不是直接回到parent的回边?
        1. && if( entry_time[w] < entry_time[ reachable_ancestor[v] ] ) ==> update reachable_ancestor[v]=w
        2. 注意,这里的&& if实际上是为了排除可能的CROSS边情形(此时w还未被访问到!)
      3. 3种割点:Root- Bridge- Parent-
        1. OK,还需要在process_vertex_late(v)里做进一步判断(略,-_-)
        2. 割边(桥)=〉无,‘边-biconnected’
    5. p178 Forward边?不就是Tree边嘛,是不是有误?
  8. 有向图上的DFS
    1. 新的边分类:edge_classification(v,w):
      1. if( v==parent[w] ) return TREE;
      2. if( discovered[w] && !processed[w] ) return BACK;
      3. if( processed[w] && entry_time[w]>entry_time[v] ) return FORWARD; //(在遍历v的其余子女节点时已经作为v的后代节点被访问过)<== BFS不可能存在FORWARD边
      4. if( processed[w] && entry_time[w]<entry_time[v] ) return CROSS; //要求w已访问过(但未处理)
    2. 拓扑排序(前提:无BACK边)
      1. Labeling vertices in the reverse order that they are marked processed(?)
    3. 强连通分量scc(关键是这里的分情况讨论)
      1. ‘active’栈
      2. low[v]:v所在连通分量最早的节点代表
      3. scc[v]:v所在连通分量的编号
      4. 分析:low[v]不一定是v的祖先,可能对应CROSS边;从一个scc到另一个的CROSS边无助于连接2个scc;忽略FORWARD边
      5. 一个新的scc被发现,只要low[v]==v

加权图

  1. MST
    1. Prim
    2. Kruskal
      1. Union-Find
  2. War Story:Nothing but Nets
    1. break big nets into small nets?
    2. L^infinite metric
  3. 最短路径
    1. Dijkstra(与Prim只差3行代码):... is how they rate the desirablility of each outside vertex
    2. 全源:Floyd,3重循环k,i,j
  4. War Story:Dialing for Documents(用电话号码0-9来输入字母a-z)
    1. 实际上,这个问题似乎有个缺陷,容错性不佳(未考虑用户可能输入有错误的情形,正如设计一个容错性高的parser...)
    2. N-Gram,语句级别反歧义:归约到最短路径,DAG上的最短路径DP称为Viterbi算法
  5. 网络流与最大二部图匹配(Bipartite Matching)
    1. 后者归约到前者(创建2个连接到所有顶点的source/sink节点)
    2. ‘residual flow graph’
      1. s-t间的最大流=最小割(注意,割指的是删除后使得s-t不连通的边集)
      2. netflow(增广路径法):bfs path_volume augment_path find_edge
        1. 暂略(TODO)
  6. 设计图而非算法(归约)
    1. Stop and Think:把平面上的矩形划分为子集和,其中每个集合中的矩阵互不重叠
      1. 把每个矩形作为图中节点,‘重叠’关系视为边,则可转化为最小顶点着色into独立集问题
    2. Stop and Think:DOS长文件名重命名问题
      1. 注意:生成每个长文件名的所有简写形式,然后寻找原文件名与简写之间的‘(最大)二部图匹配’
    3. Stop and Think:OCR应用中的字符区域segmentation问题
      1. 转化为最短路径

组合搜索与启发式

  1. 回溯
    1. backtrack:注意for(i : candidates){ make_move(); 递归 unmake_move(); }部分(~permutation)
      1. ncandidates = construct_candidates(A, k, n, candidates) //构造第k步的候选移动集合
        1. candidates_set实际上是backtrack每步递归时产生的局部变量,因此backtrack深层嵌套递归时栈开销很大
    2. 构造(枚举){1...n}的所有子集
      1. 第k个元素只有“选中/不选中”2个选择,因此candidates可用bool[2]数组表示
  2. 剪枝
    1. 增加约束条件判断
    2. 利用对称性引入‘序’(也可理解为做复杂事情时的特殊的先后步骤)
  3. Sudoku
    1. 候选集优先顺序(如果目标是尽快找到一个可行解的话):选择约束最多的
    2. ‘Look ahead’?这个具体怎么做有点难度(alpha-beta剪枝似乎也可归类为本策略?)
  4. War Story:Covering Chessboards
  5. 启发式搜索
    1. 搜索空间表示 & 代价函数
    2. 随机采样(Monte carlo)
      1. 有可能以均匀的概率发现目标解,从而在平均意义上更快地找到第一个解?
      2. p250 Uniformly生成随机数:i=random(1,n-1); j=random(i+1,n); 注意i<j
    3. 梯度下降(局部搜索,hill_climbing)
      1. 某种理论依据:解可能在局部聚集(great coherence)
    4. 模拟退火
      1. 暂略(TODO)
  6. (TODO)War Story:Only it is Not a Radio
  7. (TODO)War Story:Annealing Arrays
  8. 其他启发式
    1. GA
    2. PA(并行)
  9. 并行算法
  10. War Story:Going Nowhere Fast:均衡负载的重要性!
  11. 练习
    1. 7-1 Multiset的排列算法?
    2. *7-6 turnpike重建:已知n个数的两两和,反向求这n个数(靠,这个问题太有难度了!)

DP

  1. 缓存 vs 计算
    1. 二项式系数:C(n,k) = C(n-1, k-1) + C(n-1, k) <== 如何根据这个递推公式编写DP代码?
  2. 近似字符串匹配(编辑距离:替换/插入/删除)
    1. me:可以从LCS上扩展思考(不过Skiena的思路是先有‘编辑距离’,然后约简到LCS)
    2. 递归的string_compare:
      1. 注意,求编辑距离时2个字符串s,t可认为是对称的,也可以看作是“从s变到t”
      2. opt[MATCH] = string_compare(s,t,i-1,j-1) + cost_replace(s[i], t[j])
      3. opt[INSERT] = string_compare(s,t,i,j-1) + cost_delete(t[j])
      4. opt[DELETE] = string_compare(s,t,i-1,j) + cost_delete(s[i]) #注意,这里下标INSERT/DELETE的说法不对称
      5. lowest_cost=min( opt[MATCH / INSERT / DELETE] )
    3. reconstruct_path:因为之前的lowest_cost只选择一个,所以这里的反向重建路径也只有一条!
  3. LIS(最长递增子序列)
    1. 如果是‘最长的连续递增子序列(run)’,则可以简单地O(n)扫描
    2. 需要知道lis(s[1..n])的extent =〉l(0)=0, l(i)=max_{0<j<i} l(j) + 1, where s[j]< s[i](嗯?)
    3. DP:需要存储l(i)及对应的前驱p(i)
    4. 可以用LCS求LIS
  4. War Story:Evolution of the Lobster
    1. 最优Morphing:关键在于定义代价函数
  5. Partition问题
    1. 一维线性分组:定义划分的代价为所有划分的最大元素和(此值越小,说明负载划分越均匀!)
  6. 解析CFG(CYK算法)
    1. CFG的Chomsky规范形式:只有X->YZ、X->a两种形式的规则
    2. 定义M[i,j,X]=true,iff S[i..j]可被X生成 =〉O(n^3)解析
    3. 最小加权的Triangulation(此问题与CFG解析有类似的DP递推公式)
  7. DP的限制:TSP
  8. *War Story:What‘s Past is Prolog
    1. 必须保证固定的规则顺序这一约束条件成了能够DP的正确性保证
  9. War Story:条形码文本压缩
  10. 练习
    1. 8-3 最长公共子字符串:如何从编辑距离得到?更简单的不依赖于DP的算法??
    2. *8-10 整数划分(分为2个和相等的子集)
    3. *8-15 扔鸡蛋
    4. *8-21 最大连续子序列和
    5. *8-22 给定指定字母的*运算表,*既不交换也不结合(都不是一个群?),求给定字母序列的加()方案,使得结果运算=a

Intractable问题与近似算法

  1. 问题与归约
  2. 算法归约
  3. 基础困难归约(SAT?)
  4. 可满足性(SAT)
  5. 创造性归约
    1. 3-SAT => IP
    2. 3-SAT => Vertex Cover(比前一个更困难?)
  6. 困难证明的艺术
    1. 源问题:3-SAT 整数划分 顶点覆盖 哈密顿回路
  7. War Story:Hard Against the Clock
    1. 把3-SAT归约到‘带条件赋值程序的不等价性’(问是否存在一个初始赋值方案,使得2个程序对某些变量的最终状态相同)
  8. War Story:And Then I Failed
    1. 顶点覆盖 => ‘单连通子图’(图的有向边(弧)子集,使得任意2顶点之间存在至多1条路径)
  9. P vs. NP
  10. 处理NP-complete问题
    1. 近似顶点覆盖
    2. 欧拉TSP
    3. 最大DAG子图
    4. Set Cover
  11. Notes
    1. 最好的介绍NP完全理论的书:Garey and Johnson: Computers and Intractability
  12. 练习
    1. 9-9 MST带约束‘所有顶点度数<=k’:NP-hard
      1. 如果要求‘存在某顶点度数>k’,则存在有效算法
    2. 9-10 稠密子图 is NP-complete
    3. 9-13 Hitting Set is NP-complete
    4. 9-16 边加权的最长路径(顶点至多访问1次) is NP-complete
    5. 9-20 Feedback Vertex Set is NP-complete

怎样设计算法

数据结构

  1. 字典
    1. 搜索树:‘局部旋转’
      1. p370 AVL和2-3树似乎过时了,红黑树更流行;伸展树允许更快的搜索(数据访问的局部性原理)
      2. B-树:Cache-oblivious数据结构?
  2. 优先队列
    1. Bouned高度的优先队列(即Sorted-Array-with-ListElement)
    2. 复杂的Fibonacci & pairing堆:用于加速decrease-key操作(?)
    3. 双端优先队列?
  3. 后缀树、后缀数组
    1. O(n)空间优化:压缩‘简单路径’,边上标记一个连续子序列s[i..j]
    2. O(n)的后缀树构造算法是非平凡的(作者没细讲)
    3. 后缀数组:每个元素只需要存储字符串后缀的起始位置指针,因此空间开销O(n);但初始构造需要排序
      1. space/time高效的算法直接构造后缀数组???
    1. Hypergraph
  4. 集合
    1. Bloom filters
  5. Kd树

数值问题

  1. 解线性方程
  2. *带宽归约:所有顶点都在一条直线上时最小化最大的顶点距离?(这个问题描述太含糊不清了)
  3. 矩阵乘法:略
  4. Determinants and Permanents:略
  5. 有约束和无约束的优化:略
  6. LP
    1. p414 LP is P-complete under log-space归约,这使得不可能有一个NC平行算法(???)
  7. 随机数生成:略
  8. 因式分解与素性测试:略
  9. 任意精度算术:略
  10. 背包(Knapsack):略
  11. DFT:略

组合问题

  1. 排序
  2. 搜索
  3. 中值与选择
    1. 嗯,得复习一下那个O(n)的Selection算法!
  4. 生成排列
    1. n!
    2. Rank(p) Unrank(m,n)
    3. 随机排列:为什么是swap(A[i], A[Random[i..n]]) #see TAOCP?
    4. C++ STL:next/prev_permutation
  5. 生成子集(组合)
    1. 2^n
      1. 文法序(surprisingly difficult?)
      2. 格雷码(记不得细节了?)
      3. 二元计数(easy)
  6. 生成划分
    1. 整数划分
    2. 集合划分
      1. restricted growth function?,例如0,1,1,2,0,3,1定义了划分{{1,5},{2,3,7},{4},{6}}
    3. Young Tableaux(杨辉三角?好像不是)
  7. 生成图
  8. Calendrical计算
  9. 作业调度
    1. *job-shop调度:可建模为bin-packing
  10. 可满足性

图问题:P

  1. 连通分量
  2. 拓扑排序
    1. ?it is #P-complete to count the num of linear extensions of a partial order
      1. #P includes NP
  3. MST
    1. Boruvka‘s算法:可以看作Kruskal的‘顶点并行’加速版?
    2. Prim在稠密图上更快(O(n^2)),而Kruskal在稀疏图上更快(O(mlgm))
      1. 使用高级数据结构,Prim可实现到O(m+nlgn);Prim+pairing堆将是最快的实际选择...
  4. 最短路
    1. p493 最快的单源最短路径:Dijkstra+Fibonacci堆?
  5. 传递闭包与归约
  6. 匹配
  7. 欧拉圈/中国邮递员
    1. p504 de Bruijn序列:(略)
  8. 边与顶点连通
  9. 网络流
    1. 增广路径法
    2. Preflow-pushing法
  10. 美观地绘制图
  11. 绘制树
  12. 平面图检测与嵌入
    1. *每个平面图都是稀疏的
    2. 尝试将K_5 - e绘制为平面图?

图问题:困难的

  1. 独立集
  2. 顶点覆盖
  3. TSP
  4. 哈密顿回路
  5. 图划分
    1. p543 ‘Spectral partitioning’?
  6. 顶点着色
    1. 每个平面图可用至多6种颜色着色
    2. 求G的chromatic数是NP完全的
  7. 边着色
    1. Viking:设图的最大顶点度=D,则G可(D+1)-边着色
  8. 图同态(Isomorphism)
    1. 没有找到有效的多项式算法,但也无法证明它是NP-完全的!
  9. Steiner树
  10. Feedback Edge/Vertex Set

计算几何

  1. 鲁棒的几何原语
  2. 凸包
  3. 三角剖分(与Voronoi是对偶问题)
  4. Voronoi
  5. 最近邻搜索
  6. 范围搜索
  7. 点定位
  8. 相交检测
  9. Bin Packing
  10. 中轴转换
  11. 多边形剖分
    1. p602 凸形分解的Hertel-Mehlhorn启发式?
  12. Simplifying Polygons(不明白在说什么,计算几何的许多问题实际上是不够精确的)
  13. 形状相似度
  14. 运动规划
  15. 维护Line Arrangements
  16. Minkowski Sum

集合与字符串

  1. 集合覆盖
    1. p622 It is instructive to show how to model vertex cover as an instance of set cover
      1. Hitting set is dual to set cover.
  2. Set Packing
  3. 字符串匹配
    1. Aho-Corasick:把模式表示为状态机
    2. 经典的KMP、BM
  4. 近似字符串匹配
  5. 文本压缩
    1. BWT(Burrows-Wheeler转换)
    2. LZW, Lempel-Ziv
    3. 实际应用:gz --> bz2 --> xz?
  6. 加密
  7. FSM最小化
    1. NFA转换为DFA:PSPACE-hard
    2. p648 对长度为m的模式,对应的DFA状态可能指数增长(由于那个* Kleene闭包),但匹配只须O(n)
  8. LCS(子串1/子序列2)
    1. O(n),后缀树:把叶子节点标记为输入的哪个字符串,然后做一个DFS求得具有所有字符串作为后代的deepest节点
    2. O(nm),编辑距离的特例(禁止替换操作):edit_distance(P,T)=n+m-2|lcs(P,T)|
      1. 反向回溯:Max(n,m)
      2. LIS:=LCS(s, sort(s))
        1. 求1..n的某排列的lis?
      3. 多个字符串(多序列对齐):O((2n)^k),NP-complete
        1. wait,最长公共子序列其实不就是diff算法的核心嘛
      4. 2个随机的n长字符串的LCS的期望值?
  9. Shortest Common Superstring, SCS
    1. 可应用于稀疏矩阵压缩?
    2. DNA序列组装?
    3. 可归约到(不对称的)TSP,“much harder”
    4. 贪心启发式:近似的SCS,可能2倍的最优,但不会坏于3.5倍最优
    5. 加入‘禁则’:NP-complete

算法资源

  1. Great artists steal.
  2. C++程序库:LEDA(这个似乎是商业授权的)CGAL Boost.Graph GOBLIN Netlib Standford-GraphBase Combinatorica 

算法设计手册(第2版)读书笔记, Springer - The Algorithm Design Manual, 2ed Steven S.Skiena 2008