第10章 算法设计技巧
哈夫曼算法
算法对由树组成的一个森林进行。一棵树的权等于它的树叶的频率的和。任意选取最小权的两棵树T1和T2,并任意形成以T1和 T2为子树的新树,将这样的过程进行C - 1次。在算法的开始,存在C棵单节点树——每个字符一棵。在算法结束时得到一棵树,这棵树就是最优哈夫曼编码树。
10.1.3 近似装箱问题
联机算法:
每一件物品必须放入一个箱子之后才能处理下一件物品。
存在使得任意联机装箱算法至少使用4/3最优箱子数的输入。
脱机算法:
在一个脱机装箱算法中,我们做任何事都需要等到所有的输入数据全部读取之后才进行。
下项适合算法:
当处理任何一项物品时,我们检查看它是否还能装进刚刚装进物品的同一个箱子中去。如果能够装进去,那么就把它放入该箱中;否则,就开辟一个新的箱子。
令M是讲一列物品I装箱所需的最有装箱数,则下项适合算法所用箱数绝不超过2M个箱子。存在一些顺序使得下项适合算法用箱数达2M - 2个。
首次适合算法:
依序扫描这些箱子并把新的一项物品放入足能盛下它的第一个箱子中。
令M是将一列物品I装箱所需要的最优箱子数,则首次适合算法使用的箱子绝不多于17/10 M[向上取整]。存在使得首次适合算法使用17/10(M - 1)[向上取整]个箱子的序列。
最佳适合算法:
该算法不是把一项新物品放入所发现的第一个能容纳它的箱子,而是放到所有箱子中能够容纳它的最满的箱子中。
所有联机算法的主要问题在于将大项物品装箱困难,特别是当它们在输入的后期出现的时候。围绕这个问题的自然方法是将各项物品排序,把最大的物品放在最先。此时我们可以应用首次适合算法或最佳适合算法,分别得到首次适合递减算法和最佳适合递减算法。
10.2 分治算法
分(divide):递归解决较小的问题(当然,基本情况除外)。
治(conque):然后从子问题的解构建原问题的解。
10.2.2 最近点问题
min(&, dc)的精化计算
1 /** 2 * // Points are all in the strip and sorted by y-coordinate 3 * 4 * for(int i = 0; i < numPointsInStrip; i++) 5 * for(j = i + 1; j < numPointsInStrip; j++) 6 * if(pi and pj's y-coordinates differ by more than &) 7 * break; // Go to next pi. 8 * else 9 * if(dist(pi, pj) < &) 10 * & = dist(pi, pj); 11 */
我们将保留两个表。一个是按照x坐标排序的点的表,而另一个是按照y坐标排序的点的表。我们分别称这两个表为P和Q。这两个表可以通过一个预处理排序步骤花费O(NlogN)得到,因此并不影响时间界。PL和PL是传递给左半部分递归调用的参数表,PR和QR是传递给右半部分递归调用的参数表。我们已经看到,P很容易在中间分开。一旦分割线已知,我们依序转到Q,把每一个元素放入相应的QL或QR。容易看出,QL和QR将自动地按照y坐标排序。当递归调用返回时,我们扫描Q表并删除其x坐标不在带内的所有的点。此时Q只含有带中的点,而这些点保证是按照它们的y坐标排序的。
10.2.3 选择问题
五数中值取中分割法
1. 把N个元素分成N/5[向下取整]组5个元素的组,忽略剩余(最多4个)的元素。
2. 找出每组的中值项,得到N/5[向下取整]个中值项的表M。
3. 求出M的中值项,将其作为枢纽元v返回。
降低比较的平均次数:分治算法还可以用来降低选择算法所需要的期望比较次数。
整数相乘
XL*YR + XR*YL = (XL - XR)*(YR - YL) + XL*YL + XR*YR
矩阵乘法
简单的O(N^3)矩阵乘法
1 /** 2 * Standard matrix multiplication. 3 * Arrays start at 0. 4 * Assumes a and b are square. 5 */ 6 public static int[][] multiply(int[][] a, int[][] b) 7 { 8 int n = a.length; 9 int[][] c = new int[n][n]; 10 11 for (int i = 0; i < n; i++) // Initialization 12 for (int j = 0; j < n; j++) 13 c[i][j] = 0; 14 15 for (int i = 0; i < n; i++) 16 for (int j = 0; j < n; j++) 17 for (int k = 0; k < n; k++) 18 c[i][j] += a[i][k] * b[k][j]; 19 20 return c; 21 }
10.3 动态规划
任何数学递推公式都可以直接转换成递归算法,但是基本现实是编译器常常不能正确对待递归算法,结果导致低效的程序。当怀疑很可能是这种情况时,我们必须再给编译器提供一些帮助,将递归算法重新写成非递归算法,让后者把那些子问题的答案系统地记录在一个表内。利用这种方法的技巧叫作动态规划。
计算斐波那契数的低效算法
1 public static int fib(int n) 2 { 3 if (n <= 1) 4 return 1; 5 else 6 return fib(n - 1) + fib(n - 2); 7 }
计算斐波那契数的线性算法
1 public static int fibonacci(int n) 2 { 3 if (n <= 1) 4 return 1; 5 6 int last = 1; 7 int nextToLast = 1; 8 int answer = 1; 9 10 for (int i = 2; i <= n; i++) 11 { 12 answer = last + nextToLast; 13 nextToLast = last; 14 last = answer; 15 } 16 return answer; 17 }
计算C(N) = (2 / N)∑C(i) + N的值的递归方法
1 public static double eval(int n) 2 { 3 if (n == 0) 4 return 1.0; 5 else 6 { 7 double sum = 0.0; 8 for (int i = 0; i < n; i++) 9 sum += eval(i); 10 return 2.0 * sum / n + n; 11 } 12 }
使用一个表来计算C(N) = (2 / N)∑C(i) + N的值
1 public static double eval1(int n) 2 { 3 double[] c = new double[n + 1]; 4 5 c[0] = 1.0; 6 for (int i = 0; i <= n; i++) 7 { 8 double sum = 0.0; 9 for (int j = 0; j < i; j++) 10 sum += c[j]; 11 c[i] = 2.0 * sum / i + i; 12 } 13 return c[n]; 14 }
找出矩阵乘法最优顺序的程序
1 /** 2 * Compute optimal ordering of matrix multiplication. 3 * c contains the number of columns for each of the n matrices. 4 * c[0] is the number of rows in matrix 1. 5 * The minimum number of multiplications is left in m[1][n]. 6 * Actual ordering is computed via another procedure using lastChange. 7 * m and lastChange are indexed starting at 1, instead of 0. 8 * Note: Entries below main diagonals of m and lastChange. 9 * are meaningless and uninitialized. 10 */ 11 public static void optMatrix(int[] c, long[][]m, int[][] lastChange) 12 { 13 int n = c.length - 1; 14 15 for (int left = 1; left <= n; left++) 16 m[left][left] = 0; 17 for (int k = 1; k < n; k++) 18 for (int left = 1; left <= n - k; left++) 19 { 20 // For each position 21 int right = left + k; 22 m[left][right] = INFINITY; 23 for (int i = left; i < right; i++) 24 { 25 long thisCost = m[left][i] + m[i + 1][right] + c[left - 1] * c[i] * c[right]; 26 if (thisCost < m[left][right]) // Update min 27 { 28 m[left][right] = thisCost; 29 lastChange[left][right] = i; 30 } 31 } 32 } 33 }
所有点对最短路径
1 static int NOT_A_VERTEX = -1; 2 /** 3 * Compute all-shortest paths. 4 * a[][] contains the adjacency matrix with 5 * a[i][i] presumed to be zero. 6 * d[] contains the values of the shortest path. 7 * Vertices are numbered starting at 0; all arrays 8 * have equal dimension. A negative cycle exists if 9 * d[i][i] is set to a negative value. 10 * Actual path can be computed using path[][]. 11 * NOT_A_VERTEX is -1 12 */ 13 public static void allPairs(int[][] a, int[][] d, int[][] path) 14 { 15 int n = a.length - 1; 16 17 // Initialize d and path 18 for (int i = 0; i < n; i++) 19 for (int j = 0; j < n; j++) 20 { 21 d[i][j] = a[i][j]; 22 path[i][j] = NOT_A_VERTEX; 23 } 24 25 for (int k = 0; k < n; k++) 26 // Consider each vertex as an intermediate 27 for (int i = 0; i < n; i++) 28 for (int j = 0; j < n; j++) 29 if (d[i][k] + d[k][j] < d[i][j]) 30 { 31 // Update shortest path 32 d[i][j] = d[i][k] + d[k][j]; 33 path[i][j] = k; 34 } 35 }
不溢出的随机数发生器
1 // CONSTRUCTION: with (a) no initializer or (b) an integer 2 // that specifies the initial state of the generator 3 // 4 // Return a random number according to some distribution: 5 // int randomInt() 6 // int random0_1() 7 // int randInt(int low, int high) 8 // long randomLong(long low, long high) 9 // void permute(Object[] a) 10 11 /** 12 * Random number class, using a 31-bit 13 * linear congruential generator. 14 * Note that java.util contains a class Random 15 * so watch out for name conflicts. 16 * @author Mark Allen Weiss 17 */ 18 public class Random 19 { 20 private static final int A = 48271; 21 private static final int M = 2147483647; 22 private static final int Q = M / A; 23 private static final int R = M % A; 24 25 /** 26 * Construct this Random object with 27 * initial state obtained from system clock. 28 */ 29 public Random(){ this((int)(System.currentTimeMillis() % Integer.MAX_VALUE)); } 30 31 /** 32 * Construct this Random object with 33 * specified initial state 34 * @param initialValue the initial state 35 */ 36 public Random(int initialValue) 37 { 38 if (initialValue < 0) 39 initialValue += M; 40 41 state = initialValue; 42 if (state == 0) 43 state = 1; 44 } 45 46 /** 47 * Return a pseudorandom int, and change the 48 * internal state 49 * @return the pseudorandom int. 50 */ 51 public int randomInt() 52 { 53 int tmpState = A * (state % Q) - R * (state / Q); 54 if (tmpState > 0) 55 state = tmpState; 56 else 57 state = tmpState + M; 58 59 return state; 60 } 61 62 /** 63 * Return a pseudorandom and change the 64 * internal state: DOES NOT WORK. 65 * @return the pseudorandom int. 66 */ 67 public int randIntWRONG(){ return state = (A * state) % M; } 68 69 /** 70 * Return a pseudorandom double in the open 0..1 71 * and change the internal state. 72 * @return the pseudorandom double 73 */ 74 public double random0_1(){ return (double) randomInt() / M; } 75 76 /** 77 * Return an int in the closed range [low, high], and 78 * change the internal state. 79 * @param low the minimum value returned 80 * @param high the maximum value returned. 81 * @return the pseudorandom int. 82 */ 83 public int randomInt(int low, int high) 84 { 85 double partitionSize = (double) M / (high - low + 1); 86 87 return (int)(randomInt() / partitionSize) + low; 88 } 89 90 /** 91 * Random an long in the closed range[low,high], and 92 * change the internal state 93 * @param low the minimum value returned 94 * @param high the maximum value returned 95 * @return the pseudorandom long. 96 */ 97 public long randomLong(long low, long high) 98 { 99 long longVal = ((long)randomInt() << 31) + randomInt(); 100 long longM = ((long) M << 31) + M; 101 102 double partitionSize = (double) longM / (high - low + 1); 103 return (long)(longVal / partitionSize) + low; 104 } 105 106 private int state; 107 }
48比特随机数发生器
1 /** 2 * Random number class, using a 48-bit 3 * linear congruential generator. 4 */ 5 public class Random48 6 { 7 private static final long A = 25_214_903_917L; 8 private static final long B = 48; 9 private static final long C = 11; 10 private static final long M = (1L<<B); 11 private static final long MASK = M - 1; 12 13 public Random48(){ state = System.nanoTime() & MASK; } 14 15 public int randomInt() { return next(32); } 16 17 public double random0_1() 18 { return ((long)(next(26)) << 27) + next(27) / (double) (1L << 53); } 19 20 /** 21 * Return specified number of random bits 22 * @param bits number of bits to return 23 * @return specified random bits 24 * @throws IllegalArgumentException if bits is more than 32 25 */ 26 private int next(int bits) 27 { 28 if (bits <= 0 || bits > 32) 29 throw new IllegalArgumentException(); 30 31 state = (A * state + C) & MASK; 32 33 return (int) (state >>> (B - bits)); 34 } 35 private long state; 36 }
10.4.3 素性测试
一种概率素性测试算法(伪码)
1 public class witness 2 { 3 /** 4 * Method that implements the basic primality test. 5 * If witness does not return 1, n is definitely composite. 6 * Do this by computing a^i (mod n) and looking for 7 * nontrivial square roots of 1 along the way 8 */ 9 private static long witness(long a, long i, long n) 10 { 11 if (i == 0) 12 return 1; 13 14 long x = witness(a, i / 2, n); 15 if (x == 0) // If n is recursively composite, stop 16 return 0; 17 18 // n is not prime if we find a nontrivial square root of 1 19 long y = (x * x) % n; 20 if (y == 1 && x != 1 && x != n - 1) 21 return 0; 22 23 if (i % 2 != 0) 24 y = (a * y) % n; 25 26 return y; 27 } 28 29 /** 30 * The number of witnesses queried in randomized primality test. 31 */ 32 public static final int TRIALS = 5; 33 34 /** 35 * Randomized primality test. 36 * Adjust TRIALS to increase confidence level 37 * @param n the number to test. 38 * @return if false, n is definitely no prime. 39 * If true, n is probably prime. 40 */ 41 public static boolean isPrime(long n) 42 { 43 Random r = new Random(); 44 45 for (int counter = 0; counter < TRIALS; counter++) 46 if (witness(r.randomLong(2, n - 2), n - 1, n) != 1) 47 return false; 48 49 return true; 50 } 51 }€
收费公路重建算法:驱动例程(伪代码)
1 boolean turnpike(int[] x, DistSet d, int n) 2 { 3 x[1] = 0; 4 x[n] = d.deleteMax(); 5 x[n - 1] = d.deleteMax(); 6 if (x[n] - x[n - 1] € d) 7 { 8 d.remove(x[n] - x[n - 1]); 9 return place(x, d, n, 2, n - 2); 10 } 11 else 12 return false; 13 }
收费公路重建算法:回溯的步骤(伪代码)
1 /** 2 * Backtracking algorithm to place the points x[left] ... x[right]. 3 * x[1]...x[left - 1] and x[right + 1]...x[n] already tentatively placed. 4 * If place returns true, then x[left]...x[right] will have values. 5 */ 6 boolean place(int[] x, DistSet d, int n, int left, int right) 7 { 8 int dmax; 9 boolean found = false; 10 11 if (d.isEmpty()) 12 return true; 13 14 dmax = d.findMax(); 15 16 // Check if setting x[right] = dmax is feasible 17 if ( |x[j] - dmax| € d for all 1 <= j < left and right < j <= n ) 18 { 19 x[right] = dmax; // Try x[right] = dmax 20 for (1 <= j < left, right < j <= n) 21 d.remove( |x[j] - dmax| ); 22 found = place(x, d, n, left, right - 1); 23 24 if (!found) // Backtrack 25 for (1 <= j < left, right < j <= n) // Undo the deletion 26 d.insert(|x[j] - dmax|); 27 } 28 // If first attempt failed, try to see if setting 29 // x[left] = x[n] - dmax is feasible 30 if (!found && |x[n] - dmax - x[j]| € d for all 1 <= j < left and right < j <= n) 31 { 32 x[left] = x[n] - dmax; // Same logic as before 33 for (1 <= j < left, right < j <= n) 34 d.remove(|x[n] - dmax - d[j]|); 35 found = place(x, d, n, left + 1, right); 36 37 if (!found) // Backtrack 38 for (1 <= j < left, right < j <= n) // Undo the deletion 39 d.insert(|x[n] - dmax - x[j]|); 40 } 41 return found; 42 }
极小极大三连游戏棋算法:计算机的选择,人的选择
1 public class MoveInfo 2 { 3 public int move; 4 public int value; 5 6 public MoveInfo(int m, int v) 7 { move = m; value = v;} 8 9 /** 10 * Recursive method to find best move for computer 11 * MoveInfo.move returns a number from 1-9 indicating square. 12 * Possible evaluations satisfy COMP_LOSS < DRAW < COMP_WIN. 13 */ 14 public MoveInfo findCompMove() 15 { 16 int i, responseValue; 17 int value, bestMove = 1; 18 MoveInfo quickWinInfo; 19 20 if (fullBoard()) 21 value = DRAW; 22 else if ((quickWinInfo = immediateCompWin()) != null) 23 return quickWinInfo; 24 else 25 { 26 value = COMP_LOSS; 27 for(i = 1; i <= 9; i++) // Try each square 28 if (isEmpty(i)) 29 { 30 place(i, COMP); 31 responseValue = findHumanMove().value; 32 unplace(i); // Restore board 33 34 if (responseValue > value) 35 { 36 // Update best move 37 value = responseValue; 38 bestMove = i; 39 } 40 } 41 } 42 return new MoveInfo(bestMove, value); 43 } 44 45 public MoveInfo finfHumanMove() 46 { 47 int i, responseValue; 48 int value, bestMove = 1; 49 MoveInfo quickWinInfo; 50 51 if (fullBoard()) 52 value = DRAW; 53 else if ((quickWinInfo = immediateHumanWin()) != null ) 54 return quickWinInfo; 55 56 else 57 { 58 value = COMP_WIN; 59 for (i = 1; i <= 9; i++) // Try each square 60 { 61 if (isEmpty(i)) 62 { 63 place(i, HUMAN); 64 responseValue = findCompMove().value; 65 unplace(i); // Restore board 66 67 if (responseValue < value) 68 { 69 // Update best move 70 value = responseValue; 71 bestMove = i; 72 } 73 } 74 } 75 } 76 return new MoveInfo(bestMove, value); 77 } 78 }
带有α-β裁减的极小极大三连游戏棋算法:计算机棋步的选择
1 /** 2 * Same as before, but perform alpha-beta pruning. 3 * The main routine should make the call with 4 * alpha = COMP_LOSS and beta = COMP_WIN. 5 */ 6 public MoveInfo findCompMove(int alpha, int beta) 7 { 8 int i, responseValue; 9 int value, bestMove = 1; 10 MoveInfo quickWinInfo; 11 12 if (fullBoard()) 13 value = DRAW; 14 else if ((quickWinInfo = immediateCompWin()) != null) 15 return quickWinInfo; 16 else 17 { 18 value = alpha; 19 for (i = 1; i <= 9 && value < beta; i++) // Try each square 20 { 21 if (isEmpty(i)) 22 { 23 place(i, COMP); 24 responseValue = findHumanMove(value, beta).value; 25 unplace(i); // Restore board 26 27 if (responseValue > value) 28 { 29 // Update best move 30 value = responseValue; 31 bestMove = i; 32 } 33 } 34 } 35 } 36 return new MoveInfo(bestMove, value); 37 }