The Algorithm Design Manual, 2ed
Springer - The Algorithm Design Manual, 2ed Steven S.Skiena 2008
目录 |
介绍
- 求一维最长不重叠的子区间的集合:从左往右,总是选择结束时间最早的(启发式=〉贪心策略)
- 一般的问题:贪心策略的正确性如何证明?比如MST(好像忘掉了)
- War Story:Psychic Modeling(这个问题到底是什么?)
- 一开始建模为set cover,但是不对(确保winning pair不需要cover all,正确的怎么做作者没有说明)
算法设计
- p48 快速指数幂
- War Story:金字塔传奇(算法的并行加速)
- Waring‘s问题:任何整数可表示为4个整数的平方和
- (猜想)任何整数可表示为<=5个金字塔数(形如(m^3-m)/6)的和(其实就是元素满足特定约束的背包问题)
- 首先按大小排序,然后构造2个元素和的所有情形(仍然按大小排序)
- 评论:这里构造预计算集相当于空间换时间,而要求数据有序则使得可以二分搜索
- (层次递进)对数k,首先确认它是否是1818个最小的金字塔数p之一,再确认它是否是2个金字塔数的和two之一(二分搜索!)
- 要确认是否是3个金字塔数的和之一,这时需要分治:首先从k中减去p[i];复杂度O(n^(1/3) lgn)
- 4个情况,分为‘2+2’,即k-two[i]
- 总复杂度O(n^(4/3) lgn),相比原来的穷尽搜索(O(n^2))
- 首先按大小排序,然后构造2个元素和的所有情形(仍然按大小排序)
- Esoteric函数
- 反向Ackerman:invA(n)‘缓增’
- loglog n
- logn / loglog n(n个叶子节点的二叉树的高度)
- log^2 n
数据结构
- Contiguous vs Linked
- 栈与队列
- 字典
- BST
- 平衡搜索树
- 优先队列
- War Story:Stripping Triangulations(把3D模型的三角剖分曲面数据处理为条带)
- 把每个三角形映射为图的顶点
- 注意,所谓的‘strip’就是指2个三角形共享一条边,以‘边共享’作为图的顶点邻接关系
- 则本优化问题相当于求图的最短Hamiltonian路径,NP-complete
- 启发式:to find the largest strip to peel off next
- 哈希与字符串
- 冲突消解:chaining / open-addressing?(实际上开地址法遇到桶溢出是很不划算的,但chaining有一般的链表维护开销)
- 字符串匹配的Rabin-Karp算法:如果2个字符串相同,则它们的长度和hash应该相等
- 注意,这里源字符串的hash计算是‘滚动式’的——‘滑动窗口hash’(要求hash函数基于线性加权取模)
- 取模为某素数M,则hash碰撞的可能性为1/M=〉随机算法?
- 注意,这里源字符串的hash计算是‘滚动式’的——‘滑动窗口hash’(要求hash函数基于线性加权取模)
- Specialized DS
- War Story:String ‘em Up
- DNA序列:(A, C, T, G)*
- sequencing by hybridization(SBH):确信k-子串集,构造可能的2k-子串集,如AC, CA, CC => ACCA可能,CAAC不可能
- BST --> hashtable -> 后缀树 -> 压缩后缀树 ->
排序和搜索
- 排序的应用:求点集的凸包(Convex Hull)
- 按x从左到右排序,然后依次插入目标凸包集合(维护一个上下高度范围约束?这里似乎没讲清楚)
- HeapSort
- 可以视为插入排序的变形:pg_insert -> bubble_up(当然,如果反方向的话可以bubble_down)
- 用最小堆来排序:extract_min,这时堆顶出现了空洞,把最后一个(right-most)元素补进来,bubble_down
- 注意,这里初始构造堆可以bubble_up也可以bubble_down,但中间过程heap_adjust总是bubble_down
- 考虑到如果使用最小堆排序的话,每次取出的当前最小元素放哪里?这样看来还是用最大堆排序更合适点,作者的代码:
- 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);
}
- heapsort(item_type s[], int n){//这里使用了额外的空间,如果用最大堆就可以in-place排序了
- 考虑到如果使用最小堆排序的话,每次取出的当前最小元素放哪里?这样看来还是用最大堆排序更合适点,作者的代码:
- 更快的建堆:for(int i=q->n; i>=1; --i) bubble_down(q,i); //搞不明白作者这里的priority_queue内部元素下标为什么从1开始?
- Stop and Think:如何O(k)判断堆中第k小的元素是否>=指定x?=> 转化为在层次1..k范围内统计<x的元素计数!
- 低效的做法:extract_min k次以得到kth smallest;
- 第k小的元素的层次不可能大于k:
在此范围内线性扫描?
- 第k小的元素的层次不可能大于k:
- 递归的compare_heap:传入当前已经>=x的计数count,返回修改累计后的新count
- 结束条件:if( count<=0 || i> q->n) return count;
- 注意,这里count概念的特殊传参是一个技巧
- 低效的做法:extract_min k次以得到kth smallest;
- War Story:Give me a Ticket on an Airplane(最便宜的飞机票路径规划?)
- X+Y排序(X,Y是有序列表,求所有X+Y的有序列表)
- 即时的增量计算结果 => 如果要全局最优,则需要维护一个理论上没上限的缓存(考虑到重复值的情况)
- 问题是实际情况并不满足这种简单的约束!
- MergeSort
- 这里作者再次使用了额外的queue
- 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);
- }
- int partition(item_type s[], int l, int h){
- 桶排序
- War Story:Skiena for the Defense
- 外排序:多路归并
- 二分查找与相关算法
- O(lgn)统计相同元素的出现次数:BS去除if(A[middle]==key)return middle;一句,分别用<和>=作为comparator,得到上下界
- 前提是元素已经有序?嗯,不是很稀奇
- 单侧二分查找:同样不稀奇
- O(lgn)统计相同元素的出现次数:BS去除if(A[middle]==key)return middle;一句,分别用<和>=作为comparator,得到上下界
- 分治
- master定理:T(n)=aT(n/b)+f(n),注意loga/logb与f(n)次数的关系
图遍历
- 2种表示:邻接矩阵、邻接表
- War Story:I was a Victim of Moore‘s Law
- To make a program run faster, just wait(哈哈)
- War Story:Getting the Graph(注意数据集的内在关联性)
- 顶点状态:
- undiscovered
- discovered(已访问过,但它的incident边没有全部处理)
- processed
- 3个回调时机:
- process_vertex_early(v)
- process_edge(v, w)
- process_vertex_late(v)
- BFS
- 例子:判断是否可顶点2-着色(即是否是二部图), process_edge(v, w): //这里color[v]默认着色第一种
- if( color[v]==color[w]) bipartite=FALSE;
- color[w] = complement( color[w] );
- 例子:判断是否可顶点2-着色(即是否是二部图), process_edge(v, w): //这里color[v]默认着色第一种
- 无向图上的DFS
- 边的区分:树边(parent->child)、回边(child->parent)、cross边(到非parent节点的边)
- 额外的entry_time[v]和exit_time[v]?
- 无向图找圈:if( parent[v]!=w )(回边,if写法有点怪异) //注意,在dfs框架里,parent[w]==v 或 ‘w已访问过但未处理’
- Articulation顶点(割点?去除会导致图不再弱连通)
- reachable_ancestor[v] v的最早(?)可达祖先,初始reachable_ancestor[v]=v
- 核心判断条件:if( class(v,y)==BACK_EDGE && parent[v]!=w ) //不是直接回到parent的回边?
- && if( entry_time[w] < entry_time[ reachable_ancestor[v] ] ) ==> update reachable_ancestor[v]=w
- 注意,这里的&& if实际上是为了排除可能的CROSS边情形(此时w还未被访问到!)
- 3种割点:Root- Bridge- Parent-
- OK,还需要在process_vertex_late(v)里做进一步判断(略,-_-)
- 割边(桥)=〉无,‘边-biconnected’
- p178 Forward边?不就是Tree边嘛,是不是有误?
- 有向图上的DFS
- 新的边分类:edge_classification(v,w):
- if( v==parent[w] ) return TREE;
- if( discovered[w] && !processed[w] ) return BACK;
- if( processed[w] && entry_time[w]>entry_time[v] ) return FORWARD; //(在遍历v的其余子女节点时已经作为v的后代节点被访问过)<== BFS不可能存在FORWARD边
- if( processed[w] && entry_time[w]<entry_time[v] ) return CROSS; //要求w已访问过(但未处理)
- 拓扑排序(前提:无BACK边)
- Labeling vertices in the reverse order that they are marked processed(?)
- 强连通分量scc(关键是这里的分情况讨论)
- ‘active’栈
- low[v]:v所在连通分量最早的节点代表
- scc[v]:v所在连通分量的编号
- 分析:low[v]不一定是v的祖先,可能对应CROSS边;从一个scc到另一个的CROSS边无助于连接2个scc;忽略FORWARD边
- 一个新的scc被发现,只要low[v]==v
- 新的边分类:edge_classification(v,w):
加权图
- MST
- Prim
- Kruskal
- Union-Find
- War Story:Nothing but Nets
- break big nets into small nets?
- L^infinite metric
- 最短路径
- Dijkstra(与Prim只差3行代码):... is how they rate the desirablility of each outside vertex
- 全源:Floyd,3重循环k,i,j
- War Story:Dialing for Documents(用电话号码0-9来输入字母a-z)
- 实际上,这个问题似乎有个缺陷,容错性不佳(未考虑用户可能输入有错误的情形,正如设计一个容错性高的parser...)
- N-Gram,语句级别反歧义:归约到最短路径,DAG上的最短路径DP称为Viterbi算法
- 网络流与最大二部图匹配(Bipartite Matching)
- 后者归约到前者(创建2个连接到所有顶点的source/sink节点)
- ‘residual flow graph’
- s-t间的最大流=最小割(注意,割指的是删除后使得s-t不连通的边集)
- netflow(增广路径法):bfs path_volume augment_path find_edge
- 暂略(TODO)
- 设计图而非算法(归约)
- Stop and Think:把平面上的矩形划分为子集和,其中每个集合中的矩阵互不重叠
- 把每个矩形作为图中节点,‘重叠’关系视为边,则可转化为最小顶点着色into独立集问题
- Stop and Think:DOS长文件名重命名问题
- 注意:生成每个长文件名的所有简写形式,然后寻找原文件名与简写之间的‘(最大)二部图匹配’
- Stop and Think:OCR应用中的字符区域segmentation问题
- 转化为最短路径
- Stop and Think:把平面上的矩形划分为子集和,其中每个集合中的矩阵互不重叠
组合搜索与启发式
- 回溯
- backtrack:注意for(i : candidates){ make_move(); 递归 unmake_move(); }部分(~permutation)
- ncandidates = construct_candidates(A, k, n, candidates) //构造第k步的候选移动集合
- candidates_set实际上是backtrack每步递归时产生的局部变量,因此backtrack深层嵌套递归时栈开销很大
- ncandidates = construct_candidates(A, k, n, candidates) //构造第k步的候选移动集合
- 构造(枚举){1...n}的所有子集
- 第k个元素只有“选中/不选中”2个选择,因此candidates可用bool[2]数组表示
- backtrack:注意for(i : candidates){ make_move(); 递归 unmake_move(); }部分(~permutation)
- 剪枝
- 增加约束条件判断
- 利用对称性引入‘序’(也可理解为做复杂事情时的特殊的先后步骤)
- Sudoku
- 候选集优先顺序(如果目标是尽快找到一个可行解的话):选择约束最多的
- ‘Look ahead’?这个具体怎么做有点难度(alpha-beta剪枝似乎也可归类为本策略?)
- War Story:Covering Chessboards
- 启发式搜索
- 搜索空间表示 & 代价函数
- 随机采样(Monte carlo)
- 有可能以均匀的概率发现目标解,从而在平均意义上更快地找到第一个解?
- p250 Uniformly生成随机数:i=random(1,n-1); j=random(i+1,n); 注意i<j
- 梯度下降(局部搜索,hill_climbing)
- 某种理论依据:解可能在局部聚集(great coherence)
- 模拟退火
- 暂略(TODO)
- (TODO)War Story:Only it is Not a Radio
- (TODO)War Story:Annealing Arrays
- 其他启发式
- GA
- PA(并行)
- 并行算法
- War Story:Going Nowhere Fast:均衡负载的重要性!
- 练习
- 7-1 Multiset的排列算法?
- *7-6 turnpike重建:已知n个数的两两和,反向求这n个数(靠,这个问题太有难度了!)
DP
- 缓存 vs 计算
- 二项式系数:C(n,k) = C(n-1, k-1) + C(n-1, k) <== 如何根据这个递推公式编写DP代码?
- 近似字符串匹配(编辑距离:替换/插入/删除)
- me:可以从LCS上扩展思考(不过Skiena的思路是先有‘编辑距离’,然后约简到LCS)
- 递归的string_compare:
- 注意,求编辑距离时2个字符串s,t可认为是对称的,也可以看作是“从s变到t”
- opt[MATCH] = string_compare(s,t,i-1,j-1) + cost_replace(s[i], t[j])
- opt[INSERT] = string_compare(s,t,i,j-1) + cost_delete(t[j])
- opt[DELETE] = string_compare(s,t,i-1,j) + cost_delete(s[i]) #注意,这里下标INSERT/DELETE的说法不对称
- lowest_cost=min( opt[MATCH / INSERT / DELETE] )
- reconstruct_path:因为之前的lowest_cost只选择一个,所以这里的反向重建路径也只有一条!
- LIS(最长递增子序列)
- 如果是‘最长的连续递增子序列(run)’,则可以简单地O(n)扫描
- 需要知道lis(s[1..n])的extent =〉l(0)=0, l(i)=max_{0<j<i} l(j) + 1, where s[j]< s[i](嗯?)
- DP:需要存储l(i)及对应的前驱p(i)
- 可以用LCS求LIS
- War Story:Evolution of the Lobster
- 最优Morphing:关键在于定义代价函数
- Partition问题
- 一维线性分组:定义划分的代价为所有划分的最大元素和(此值越小,说明负载划分越均匀!)
- 解析CFG(CYK算法)
- CFG的Chomsky规范形式:只有X->YZ、X->a两种形式的规则
- 定义M[i,j,X]=true,iff S[i..j]可被X生成 =〉O(n^3)解析
- 最小加权的Triangulation(此问题与CFG解析有类似的DP递推公式)
- DP的限制:TSP
- *War Story:What‘s Past is Prolog
- 必须保证固定的规则顺序这一约束条件成了能够DP的正确性保证
- War Story:条形码文本压缩
- 练习
- 8-3 最长公共子字符串:如何从编辑距离得到?更简单的不依赖于DP的算法??
- *8-10 整数划分(分为2个和相等的子集)
- *8-15 扔鸡蛋
- *8-21 最大连续子序列和
- *8-22 给定指定字母的*运算表,*既不交换也不结合(都不是一个群?),求给定字母序列的加()方案,使得结果运算=a
Intractable问题与近似算法
- 问题与归约
- 算法归约
- 基础困难归约(SAT?)
- 可满足性(SAT)
- 创造性归约
- 3-SAT => IP
- 3-SAT => Vertex Cover(比前一个更困难?)
- 困难证明的艺术
- 源问题:3-SAT 整数划分 顶点覆盖 哈密顿回路
- War Story:Hard Against the Clock
- 把3-SAT归约到‘带条件赋值程序的不等价性’(问是否存在一个初始赋值方案,使得2个程序对某些变量的最终状态相同)
- War Story:And Then I Failed
- 顶点覆盖 => ‘单连通子图’(图的有向边(弧)子集,使得任意2顶点之间存在至多1条路径)
- P vs. NP
- 处理NP-complete问题
- 近似顶点覆盖
- 欧拉TSP
- 最大DAG子图
- Set Cover
- Notes
- 最好的介绍NP完全理论的书:Garey and Johnson: Computers and Intractability
- 练习
- 9-9 MST带约束‘所有顶点度数<=k’:NP-hard
- 如果要求‘存在某顶点度数>k’,则存在有效算法
- 9-10 稠密子图 is NP-complete
- 9-13 Hitting Set is NP-complete
- 9-16 边加权的最长路径(顶点至多访问1次) is NP-complete
- 9-20 Feedback Vertex Set is NP-complete
- 9-9 MST带约束‘所有顶点度数<=k’:NP-hard
怎样设计算法
数据结构
- 字典
- 搜索树:‘局部旋转’
- p370 AVL和2-3树似乎过时了,红黑树更流行;伸展树允许更快的搜索(数据访问的局部性原理)
- B-树:Cache-oblivious数据结构?
- 搜索树:‘局部旋转’
- 优先队列
- Bouned高度的优先队列(即Sorted-Array-with-ListElement)
- 复杂的Fibonacci & pairing堆:用于加速decrease-key操作(?)
- 双端优先队列?
- 后缀树、后缀数组
- O(n)空间优化:压缩‘简单路径’,边上标记一个连续子序列s[i..j]
- O(n)的后缀树构造算法是非平凡的(作者没细讲)
- 后缀数组:每个元素只需要存储字符串后缀的起始位置指针,因此空间开销O(n);但初始构造需要排序
- space/time高效的算法直接构造后缀数组???
- 图
- Hypergraph
- 集合
- Bloom filters
- Kd树
数值问题
- 解线性方程
- *带宽归约:所有顶点都在一条直线上时最小化最大的顶点距离?(这个问题描述太含糊不清了)
- 矩阵乘法:略
- Determinants and Permanents:略
- 有约束和无约束的优化:略
- LP
- p414 LP is P-complete under log-space归约,这使得不可能有一个NC平行算法(???)
- 随机数生成:略
- 因式分解与素性测试:略
- 任意精度算术:略
- 背包(Knapsack):略
- DFT:略
组合问题
- 排序
- 搜索
- 中值与选择
- 嗯,得复习一下那个O(n)的Selection算法!
- 生成排列
- n!
- Rank(p) Unrank(m,n)
- 随机排列:为什么是swap(A[i], A[Random[i..n]]) #see TAOCP?
- C++ STL:next/prev_permutation
- 生成子集(组合)
- 2^n
- 序
- 文法序(surprisingly difficult?)
- 格雷码(记不得细节了?)
- 二元计数(easy)
- 生成划分
- 整数划分
- 集合划分
- restricted growth function?,例如0,1,1,2,0,3,1定义了划分{{1,5},{2,3,7},{4},{6}}
- Young Tableaux(杨辉三角?好像不是)
- 生成图
- Calendrical计算
- 作业调度
- *job-shop调度:可建模为bin-packing
- 可满足性
图问题:P
- 连通分量
- 拓扑排序
- ?it is #P-complete to count the num of linear extensions of a partial order
- #P includes NP
- ?it is #P-complete to count the num of linear extensions of a partial order
- MST
- Boruvka‘s算法:可以看作Kruskal的‘顶点并行’加速版?
- Prim在稠密图上更快(O(n^2)),而Kruskal在稀疏图上更快(O(mlgm))
- 使用高级数据结构,Prim可实现到O(m+nlgn);Prim+pairing堆将是最快的实际选择...
- 最短路
- p493 最快的单源最短路径:Dijkstra+Fibonacci堆?
- 传递闭包与归约
- 匹配
- 欧拉圈/中国邮递员
- p504 de Bruijn序列:(略)
- 边与顶点连通
- 网络流
- 增广路径法
- Preflow-pushing法
- 美观地绘制图
- 绘制树
- 平面图检测与嵌入
- *每个平面图都是稀疏的
- 尝试将K_5 - e绘制为平面图?
图问题:困难的
- 团
- 独立集
- 顶点覆盖
- TSP
- 哈密顿回路
- 图划分
- p543 ‘Spectral partitioning’?
- 顶点着色
- 每个平面图可用至多6种颜色着色
- 求G的chromatic数是NP完全的
- 边着色
- Viking:设图的最大顶点度=D,则G可(D+1)-边着色
- 图同态(Isomorphism)
- 没有找到有效的多项式算法,但也无法证明它是NP-完全的!
- Steiner树
- Feedback Edge/Vertex Set
计算几何
- 鲁棒的几何原语
- 凸包
- 三角剖分(与Voronoi是对偶问题)
- Voronoi
- 最近邻搜索
- 范围搜索
- 点定位
- 相交检测
- Bin Packing
- 中轴转换
- 多边形剖分
- p602 凸形分解的Hertel-Mehlhorn启发式?
- Simplifying Polygons(不明白在说什么,计算几何的许多问题实际上是不够精确的)
- 形状相似度
- 运动规划
- 维护Line Arrangements
- Minkowski Sum
集合与字符串
- 集合覆盖
- p622 It is instructive to show how to model vertex cover as an instance of set cover
- Hitting set is dual to set cover.
- p622 It is instructive to show how to model vertex cover as an instance of set cover
- Set Packing
- 字符串匹配
- Aho-Corasick:把模式表示为状态机
- 经典的KMP、BM
- 近似字符串匹配
- 文本压缩
- BWT(Burrows-Wheeler转换)
- LZW, Lempel-Ziv
- 实际应用:gz --> bz2 --> xz?
- 加密
- FSM最小化
- NFA转换为DFA:PSPACE-hard
- p648 对长度为m的模式,对应的DFA状态可能指数增长(由于那个* Kleene闭包),但匹配只须O(n)
- LCS(子串1/子序列2)
- O(n),后缀树:把叶子节点标记为输入的哪个字符串,然后做一个DFS求得具有所有字符串作为后代的deepest节点
- O(nm),编辑距离的特例(禁止替换操作):edit_distance(P,T)=n+m-2|lcs(P,T)|
- 反向回溯:Max(n,m)
- LIS:=LCS(s, sort(s))
- 求1..n的某排列的lis?
- 多个字符串(多序列对齐):O((2n)^k),NP-complete
- wait,最长公共子序列其实不就是diff算法的核心嘛
- 2个随机的n长字符串的LCS的期望值?
- Shortest Common Superstring, SCS
- 可应用于稀疏矩阵压缩?
- DNA序列组装?
- 可归约到(不对称的)TSP,“much harder”
- 贪心启发式:近似的SCS,可能2倍的最优,但不会坏于3.5倍最优
- 加入‘禁则’:NP-complete
算法资源
- Great artists steal.
- C++程序库:LEDA(这个似乎是商业授权的)CGAL Boost.Graph GOBLIN Netlib Standford-GraphBase Combinatorica
时间: 2024-10-28 20:27:05