1.递归
- 自下而上的递归
由子问题或者规模较小问题的答案,构建出规模较大问题的答案 - 自上而下的递归
思考如何将问题分解成多个子问题。同时注意子问题是否重叠。
2.分治算法
3.动态规划
中间结果需要缓存起来,以备后续使用。
4.贪心算法
5.编程题
Q1.有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶、3阶。请实现一个方法,计算小孩有多少种上楼的方式。
Ans.思路:
- 递归的思路非常直接
- 动态规划的思路需要记住已经计算过的
本质上只用记住最近三个即可:
public static int countWaysDP(int n) {
if (n < 0) {
return 0;
} else if (n == 1) {
return 1;
} else if (n == 2) {
return 2;
} else if (n == 3) {
return 4;
}
int fn3 = 1;
int fn2 = 2;
int fn1 = 4;
int i = 4;
int result = 0;
while (i <= n) {
result = fn3 + fn2 + fn1;
fn3 = fn2;
fn2 = fn1;
fn1 = result;
i++;
}
return result;
}
Q2. 有个机器人坐在X*Y网格的左上角,只能向右、向下移动。机器人从(0,0)到(X,Y)有多少种走法。
进阶:假设有些点为“禁区”,机器人不能踏足。找出一条路径,让机器人从左上角移动到右下角。
Ans.思路:
- 从数学角度而言,其实非常简单。机器人总共向右走X步,向下走Y步。然后中间向下和向右可以随意穿插。所以总的可能数目为C(X+Y, X)。
- 可以用算法实现,一种另外的角度。F(a,b) = F(a-1,b) + F(a, b-1),注意边界的时候需要首先初始化为1.(使用动态规划实现的时候)
Q3.在数组A[0 ... n-1]中,有所谓的魔术索引,满足条件A[i] = i。给定一个有序整数数组,元素值各不相同,编写一个方法,在数组A中找出一个魔术索引,若存在的话。
进阶:如果数组元素有重复值,又该如何处理?
Ans.思路:
- 本质是二分查找
- 若有重复值,就不能只搜一边,要搜索两边。注意搜索的边界,对于左边取min(midIndex-1, midValue),对于右边取max(midIndex+1, midValue)
public static int magicSlow(int[] array) {
for (int i = 0; i < array.length; i++) {
if (array[i] == i) {
return i;
}
}
return -1;
}
public static int magicFast(int[] array, int start, int end) {
if (end < start || start < 0 || end >= array.length) {
return -1;
}
int midIndex = (start + end) / 2;
int midValue = array[midIndex];
if (midValue == midIndex) {
return midIndex;
}
/* Search left */
int leftIndex = Math.min(midIndex - 1, midValue);
int left = magicFast(array, start, leftIndex);
if (left >= 0) {
return left;
}
/* Search right */
int rightIndex = Math.max(midIndex + 1, midValue);
int right = magicFast(array, rightIndex, end);
return right;
}
public static int magicFast(int[] array) {
return magicFast(array, 0, array.length - 1);
}
Q4.编写一个方法,返回某集合的所有子集
Ans.思路:
- 从数学方面看,很容易处理,对于某个元素无非取或不取,所以有2^n个子集。
- 因此可利用0 ~ (2^n - 1)的二进位转换成集合
public static ArrayList convertIntToSet(int x, ArrayList set) {
ArrayList subset = new ArrayList();
int index = 0;
for (int k = x; k > 0; k >>= 1) {
if ((k & 1) == 1) {
subset.add(set.get(index));
}
index++;
}
return subset;
}
public static ArrayList> getSubsets2(ArrayList set) {
ArrayList> allsubsets = new ArrayList>();
int max = 1 << set.size(); /* Compute 2^n */
for (int k = 0; k < max; k++) {
ArrayList subset = convertIntToSet(k, set);
allsubsets.add(subset);
}
return allsubsets;
}
Q5.编写一个方法,确定某字符串的所有排列组合。
Ans.思路:
- 从简单发现规律,分析n = 1, 2, 3时的情况。可以根据f(2)生成f(3),只要在f(2)的每种情况里面塞a3即可。
- 直接用递归实现
public static ArrayList getPerms(String str) {
if (str == null) {
return null;
}
ArrayList permutations = new ArrayList();
if (str.length() == 0) { // base case
permutations.add("");
return permutations;
}
char first = str.charAt(0); // get the first character
String remainder = str.substring(1); // remove the first character
ArrayList words = getPerms(remainder);
for (String word : words) {
for (int j = 0; j <= word.length(); j++) {
String s = insertCharAt(word, first, j);
permutations.add(s);
}
}
return permutations;
}
public static String insertCharAt(String word, char c, int i) {
String start = word.substring(0, i);
String end = word.substring(i);
return start + c + end;
}
Q6.实现一种算法,打印n对括号的全部有效组合。
Ans.思路:
- 递归解决思路:在已有的n-1对基础上加入1对,构成n对的解,但是会有重复,需要去除重复,(可以使用HashSet来去重)。有重复解,那么必然会拖累效率
- 按照内在的规律来进行:每次调用,都可以选择左括号或者右括号,但是要满足如下条件:
1)左括号没有用完,就可以插入左括号
2)只要左括号比右括号多,就可以插入右括号
使用HashSet去重:
public static String insertInside(String str, int leftIndex) {
String left = str.substring(0, leftIndex + 1);
String right = str.substring(leftIndex + 1, str.length());
return left + "()" + right;
}
public static Set generateParens(int remaining) {
Set set = new HashSet();
if (remaining == 0) {
set.add("");
} else {
Set prev = generateParens(remaining - 1);
for (String str : prev) {
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '(') {
String s = insertInside(str, i);
/* Add s to set if it is not already in there. Note:
* HashSet automatically checks for duplicates before
* adding, so an explicit check is not necessary. */
set.add(s);
}
}
set.add("()" + str);
}
}
return set;
}
根据括号排列的内在规律:
public static void addParen(ArrayList list, int leftRem, int rightRem, char[] str, int count) {
if (leftRem < 0 || rightRem < leftRem) return; // invalid state
if (leftRem == 0 && rightRem == 0) { /* all out of left and right parentheses */
String s = String.copyValueOf(str);
list.add(s);
} else {
if (leftRem > 0) { // try a left paren, if there are some available
str[count] = '(';
addParen(list, leftRem - 1, rightRem, str, count + 1);
}
if (rightRem > leftRem) { // try a right paren, if there�s a matching left
str[count] = ')';
addParen(list, leftRem, rightRem - 1, str, count + 1);
}
}
}
public static ArrayList generateParens(int count) {
char[] str = new char[count*2];
ArrayList list = new ArrayList();
addParen(list, count, count, str, 0);
return list;
}
Q7.编写函数,实现许多图片编辑软件都支持的“填充颜色”功能。给定一个屏幕(以二维数组表示,元素为颜色值)、一个点和一个新的颜色值,将新颜色填入这个点的周围区域,直到原来的颜色值全都改变。
Ans.思路:
- 这就是回溯法,每一个点都向四个方向延伸
public enum Color {
Black, White, Red, Yellow, Green
}
public static String PrintColor(Color c) {
switch(c) {
case Black:
return "B";
case White:
return "W";
case Red:
return "R";
case Yellow:
return "Y";
case Green:
return "G";
}
return "X";
}
public static void PrintScreen(Color[][] screen) {
for (int i = 0; i < screen.length; i++) {
for (int j = 0; j < screen[0].length; j++) {
System.out.print(PrintColor(screen[i][j]));
}
System.out.println();
}
}
public static int randomInt(int n) {
return (int) (Math.random() * n);
}
public static boolean PaintFill(Color[][] screen, int x, int y, Color ocolor, Color ncolor) {
if (x < 0 || x >= screen[0].length || y < 0 || y >= screen.length) {
return false;
}
if (screen[y][x] == ocolor) {
screen[y][x] = ncolor;
PaintFill(screen, x - 1, y, ocolor, ncolor); // left
PaintFill(screen, x + 1, y, ocolor, ncolor); // right
PaintFill(screen, x, y - 1, ocolor, ncolor); // top
PaintFill(screen, x, y + 1, ocolor, ncolor); // bottom
}
return true;
}
public static boolean PaintFill(Color[][] screen, int x, int y, Color ncolor) {
if (screen[y][x] == ncolor) return false;
return PaintFill(screen, x, y, screen[y][x], ncolor);
}
Q8.给定数量不定的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。
Ans.思路:
- 从25分开始枚举包括0个一直到最多4个;
针对上面每一种情况,开始枚举10分硬币;
依次类推
public static int makeChange(int amount, int[] denoms, int index) {
if (index >= denoms.length - 1) return 1; // one denom remaining -> one way to do it
int denomAmount = denoms[index];
int ways = 0;
for (int i = 0; i * denomAmount <= amount; i++) {
int amountRemaining = amount - i * denomAmount;
ways += makeChange(amountRemaining, denoms, index + 1); // go to next denom
}
return ways;
}
public static int makeChange1(int n) {
int[] denoms = {25, 10, 5, 1};
return makeChange(n, denoms, 0);
}
针对上面的递归解法,可以使用动态规划解决:
public static int makeChange2(int n) {
int[] denoms = {25, 10, 5, 1};
int[][] map = new int[n + 1][denoms.length];
return makeChange2(n, denoms, 0, map);
}
public static int makeChange2(int amount, int[] denoms, int index, int[][] map) {
if (map[amount][index] > 0) { // retrieve value
return map[amount][index];
}
if (index >= denoms.length - 1) return 1; // one denom remaining -> one way to do it
int denomAmount = denoms[index];
int ways = 0;
for (int i = 0; i * denomAmount <= amount; i++) {
// go to next denom, assuming i coins of denomAmount
int amountRemaining = amount - i * denomAmount;
ways += makeChange2(amountRemaining, denoms, index + 1, map);
}
map[amount][index] = ways;
return ways;
}
Q9.设计一种算法,打印八皇后在8x8棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。
Ans.思路:
- 典型的回溯法
public static int GRID_SIZE = 8;
/* Check if (row1, column1) is a valid spot for a queen by checking if there
* is a queen in the same column or diagonal. We don't need to check it for queens
* in the same row because the calling placeQueen only attempts to place one queen at
* a time. We know this row is empty.
*/
public static boolean checkValid(Integer[] columns, int row1, int column1) {
for (int row2 = 0; row2 < row1; row2++) {
int column2 = columns[row2];
/* Check if (row2, column2) invalidates (row1, column1) as a queen spot. */
/* Check if rows have a queen in the same column */
if (column1 == column2) {
return false;
}
/* Check diagonals: if the distance between the columns equals the distance
* between the rows, then they're in the same diagonal.
*/
int columnDistance = Math.abs(column2 - column1);
int rowDistance = row1 - row2; // row1 > row2, so no need to use absolute value
if (columnDistance == rowDistance) {
return false;
}
}
return true;
}
public static void placeQueens(int row, Integer[] columns, ArrayList results) {
if (row == GRID_SIZE) { // Found valid placement
results.add(columns.clone());
} else {
for (int col = 0; col < GRID_SIZE; col++) {
if (checkValid(columns, row, col)) {
columns[row] = col; // Place queen
placeQueens(row + 1, columns, results);
}
}
}
}
public static void clear(Integer[] columns) {
for (int i = 0; i < GRID_SIZE; i++) {
columns[i] = -1;
}
}
Q10.给你一堆n个箱子,箱子宽wi、高hi、深di。箱子不能翻转,将箱子堆起来时,下面箱子的宽度、高度和深度必须大于上面的。实现一个方法,搭出最高的一堆箱子,堆箱的高度为每个箱子高度的综合。
Ans.思路:
- 典型的回溯法
- 可以改造成动态规划,符合最优子问题等性质
public class Box {
public int width;
public int height;
public int depth;
public Box(int w, int h, int d) {
width = w;
height = h;
depth = d;
}
public boolean canBeUnder(Box b) {
if (width > b.width && height > b.height && depth > b.depth) {
return true;
}
return false;
}
public boolean canBeAbove(Box b) {
if (b == null) {
return true;
}
if (width < b.width && height < b.height && depth < b.depth) {
return true;
}
return false;
}
public String toString() {
return "Box(" + width + "," + height + "," + depth + ")";
}
}
递归方法:
public static int stackHeight(ArrayList boxes) {
if (boxes == null) {
return 0;
}
int h = 0;
for (Box b : boxes) {
h += b.height;
}
return h;
}
public static ArrayList createStackR(Box[] boxes, Box bottom) {
int max_height = 0;
ArrayList max_stack = null;
for (int i = 0; i < boxes.length; i++) {
if (boxes[i].canBeAbove(bottom)) {
ArrayList new_stack = createStackR(boxes, boxes[i]);
int new_height = stackHeight(new_stack);
if (new_height > max_height) {
max_stack = new_stack;
max_height = new_height;
}
}
}
if (max_stack == null) {
max_stack = new ArrayList();
}
if (bottom != null) {
max_stack.add(0, bottom);
}
return max_stack;
}
动态规划:
public static ArrayList createStackDP(Box[] boxes, Box bottom, HashMap> stack_map) {
if (bottom != null && stack_map.containsKey(bottom)) {
return (ArrayList) stack_map.get(bottom).clone();
}
int max_height = 0;
ArrayList max_stack = null;
for (int i = 0; i < boxes.length; i++) {
if (boxes[i].canBeAbove(bottom)) {
ArrayList new_stack = createStackDP(boxes, boxes[i], stack_map);
int new_height = stackHeight(new_stack);
if (new_height > max_height) {
max_stack = new_stack;
max_height = new_height;
}
}
}
if (max_stack == null) {
max_stack = new ArrayList();
}
if (bottom != null) {
max_stack.add(0, bottom);
}
stack_map.put(bottom, max_stack);
return max_stack;
}
Q11.给定一个布尔表达式,由0、 1、 &、 | 和 ^ 等符号组成,以及一个想要的布尔结果result,实现一个函数,算出有几种括号的放法可使该表达式得出result值。
Ans.思路:
- 一个直接的做法是使用回溯法
- 优化,针对|,可以根据卡塔兰算出总数,然后减去false false情况
public static int countR(String exp, boolean result, int start, int end) {
if (start == end) {
if (exp.charAt(start) == '1' && result) {
return 1;
} else if (exp.charAt(start) == '0' && !result) {
return 1;
}
return 0;
}
int c = 0;
if (result) {
for (int i = start + 1; i <= end; i += 2) {
char op = exp.charAt(i);
if (op == '&') {
c += countR(exp, true, start, i - 1) * countR(exp, true, i + 1, end);
} else if (op == '|') {
c += countR(exp, true, start, i - 1) * countR(exp, false, i + 1, end);
c += countR(exp, false, start, i - 1) * countR(exp, true, i + 1, end);
c += countR(exp, true, start, i - 1) * countR(exp, true, i + 1, end);
} else if (op == '^') {
c += countR(exp, true, start, i - 1) * countR(exp, false, i + 1, end);
c += countR(exp, false, start, i - 1) * countR(exp, true, i + 1, end);
}
}
} else {
for (int i = start + 1; i <= end; i += 2) {
char op = exp.charAt(i);
if (op == '&') {
c += countR(exp, false, start, i - 1) * countR(exp, true, i + 1, end);
c += countR(exp, true, start, i - 1) * countR(exp, false, i + 1, end);
c += countR(exp, false, start, i - 1) * countR(exp, false, i + 1, end);
} else if (op == '|') {
c += countR(exp, false, start, i - 1) * countR(exp, false, i + 1, end);
} else if (op == '^') {
c += countR(exp, true, start, i - 1) * countR(exp, true, i + 1, end);
c += countR(exp, false, start, i - 1) * countR(exp, false, i + 1, end);
}
}
}
return c;
}
动态规划,查表法:
public static int countDP(String exp, boolean result, int start, int end, HashMap cache) {
String key = "" + result + start + end;
if (cache.containsKey(key)) {
return cache.get(key);
}
if (start == end) {
if (exp.charAt(start) == '1' && result == true) {
return 1;
} else if (exp.charAt(start) == '0' && result == false) {
return 1;
}
return 0;
}
int count = 0;
if (result) {
for (int i = start + 1; i <= end; i += 2) {
char op = exp.charAt(i);
if (op == '&') {
count += countDP(exp, true, start, i - 1, cache) * countDP(exp, true, i + 1, end, cache);
} else if (op == '|') {
count += countDP(exp, true, start, i - 1, cache) * countDP(exp, false, i + 1, end, cache);
count += countDP(exp, false, start, i - 1, cache) * countDP(exp, true, i + 1, end, cache);
count += countDP(exp, true, start, i - 1, cache) * countDP(exp, true, i + 1, end, cache);
} else if (op == '^') {
count += countDP(exp, true, start, i - 1, cache) * countDP(exp, false, i + 1, end, cache);
count += countDP(exp, false, start, i - 1, cache) * countDP(exp, true, i + 1, end, cache);
}
}
} else {
for (int i = start + 1; i <= end; i += 2) {
char op = exp.charAt(i);
if (op == '&') {
count += countDP(exp, false, start, i - 1, cache) * countDP(exp, true, i + 1, end, cache);
count += countDP(exp, true, start, i - 1, cache) * countDP(exp, false, i + 1, end, cache);
count += countDP(exp, false, start, i - 1, cache) * countDP(exp, false, i + 1, end, cache);
} else if (op == '|') {
count += countDP(exp, false, start, i - 1, cache) * countDP(exp, false, i + 1, end, cache);
} else if (op == '^') {
count += countDP(exp, true, start, i - 1, cache) * countDP(exp, true, i + 1, end, cache);
count += countDP(exp, false, start, i - 1, cache) * countDP(exp, false, i + 1, end, cache);
}
}
}
cache.put(key, count);
return count;
}
优化:
public static int total(int n) {
// Function to return (2n) ! / ((n+1)! * n!)
// To keep the numbers small, we divide by i when possible to do evenly. If not,
// we store up the remainder and divide when possible.
long num = 1;
long rem = 1;
for (int i = 2; i <= n; i++) {
num *= (n + i);
rem *= i;
if (num % rem == 0) {
num /= rem;
rem = 1;
}
}
return (int)num;
}
public static int countDPEff(String exp, boolean result, int start, int end, HashMap cache) {
String key = "" + start + end;
int count = 0;
if (!cache.containsKey(key)) {
if (start == end) {
if (exp.charAt(start) == '1') {
count = 1;
} else {
count = 0;
}
}
for (int i = start + 1; i <= end; i += 2) {
char op = exp.charAt(i);
if (op == '&') {
count += countDPEff(exp, true, start, i - 1, cache) * countDPEff(exp, true, i + 1, end, cache);
} else if (op == '|') {
int left_ops = (i - 1 - start) / 2; // parens on left
int right_ops = (end - i - 1) / 2; // parens on right
int total_ways = total(left_ops) * total(right_ops);
int total_false = countDPEff(exp, false, start, i - 1, cache) * countDPEff(exp, false, i + 1, end, cache);
count += total_ways - total_false;
} else if (op == '^') {
count += countDPEff(exp, true, start, i - 1, cache) * countDPEff(exp, false, i + 1, end, cache);
count += countDPEff(exp, false, start, i - 1, cache) * countDPEff(exp, true, i + 1, end, cache);
}
}
cache.put(key, count);
} else {
count = cache.get(key);
}
if (result) {
return count;
} else {
int num_ops = (end - start) / 2;
return total(num_ops) - count;
}
}
Q12.删除了文档中所有空格、标点,大写变小写。给定一个字典(一组单词),设计一个算法,找出拆分一连串单词的最佳方式。这里“最佳”的定义是,解析后无法辨析的字符序列越少越好。
Ans.思路:
- 递归处理方式:在字符后插入一个空格 或者 不插入(分割 不分割两种方式),会产生大量重复解
- 为了消除重复解,可以引入表存储解
-
优化(快速剪枝):以xt开头的单词不存在,所以可以快速剪枝
对于wordEnd超过长度的,可以直接返回长度,因为其兄弟分支已经检查了该字符是否在dictionary中。
public static int parseSimple(int wordStart, int wordEnd) {
if (wordEnd >= sentence.length()) {
return wordEnd - wordStart;
}
String word = sentence.substring(wordStart, wordEnd + 1);
/* break current word */
int bestExact = parseSimple(wordEnd + 1, wordEnd + 1);
if (!dictionary.contains(word, true)) {
bestExact += word.length();
}
/* extend current word */
int bestExtend = parseSimple(wordStart, wordEnd + 1);
/* find best */
return Math.min(bestExact, bestExtend);
}
加入表存储解:
public class Result {
public int invalid = Integer.MAX_VALUE;
public String parsed = "";
public Result(int inv, String p) {
invalid = inv;
parsed = p;
}
public Result clone() {
return new Result(this.invalid, this.parsed);
}
public static Result min(Result r1, Result r2) {
if (r1 == null) {
return r2;
} else if (r2 == null) {
return r1;
}
return r2.invalid < r1.invalid ? r2 : r1;
}
}
/* incomplete code */
public static Result parse(int wordStart, int wordEnd, HashMap cache) {
if (wordEnd >= sentence.length()) {
return new Result(wordEnd - wordStart, sentence.substring(wordStart).toUpperCase());
}
if (cache.containsKey(wordStart)) {
return cache.get(wordStart).clone();
}
String currentWord = sentence.substring(wordStart, wordEnd + 1);
boolean validPartial = dictionary.contains(currentWord, false);
boolean validExact = validPartial && dictionary.contains(currentWord, true);
/* break current word */
Result bestExact = parse(wordEnd + 1, wordEnd + 1, cache);
if (validExact) {
bestExact.parsed = currentWord + " " + bestExact.parsed;
} else {
bestExact.invalid += currentWord.length();
bestExact.parsed = currentWord.toUpperCase() + " " + bestExact.parsed;
}
/* extend current word */
Result bestExtend = null;
if (validPartial) {
bestExtend = parse(wordStart, wordEnd + 1, cache);
}
/* find best */
Result best = Result.min(bestExact, bestExtend);
cache.put(wordStart, best.clone());
return best;
}
使用trie快速剪枝:
public static int parseOptimized(int wordStart, int wordEnd, Hashtable cache) {
if (wordEnd >= sentence.length()) {
return wordEnd - wordStart;
}
if (cache.containsKey(wordStart)) {
return cache.get(wordStart);
}
String currentWord = sentence.substring(wordStart, wordEnd + 1);
boolean validPartial = dictionary.contains(currentWord, false);
/* break current word */
int bestExact = parseOptimized(wordEnd + 1, wordEnd + 1, cache);
if (!validPartial || !dictionary.contains(currentWord, true)) {
bestExact += currentWord.length();
}
/* extend current word */
int bestExtend = Integer.MAX_VALUE;
if (validPartial) {
bestExtend = parseOptimized(wordStart, wordEnd + 1, cache);
}
/* find best */
int min = Math.min(bestExact, bestExtend);
cache.put(wordStart, min);
return min;
}