Archive for October 2013
这个题一开始自己做死了,后来bf给讲用stack之后就一遍过了,代码也简单很多。思路
- 先用/来split string
- 然后看每一小段,若是”.”或者是“”(说明两个/连着),不入栈;若是”..”,pop;若是正常,push
- 最后再用string builder把”/”和栈中元素一个一个连起来。
public String simplifyPath(String path) { Stack<String> s = new Stack<String>(); String[] split = path.split("/"); for (String a : split) { if (!a.equals(".") && !a.isEmpty()) { if (a.equals("..")) { if (!s.isEmpty()) { s.pop(); } } else { s.push(a); } } } StringBuilder sb = new StringBuilder(); while (!s.isEmpty()) { sb.insert(0, s.pop()); sb.insert(0, "/"); } return sb.length() == 0 ? "/" : sb.toString(); }
Hands down,这是我leetcode做的最难的题top 5。别的都是可以用算法说得通的,这题就是纯数学。我这辈子最怕的就是数学,要死要活。
几个要点:
- 找没用过的里面的第(k – 1) / (n – 1)!个数字,放第一位上
- k = (k – 1) % (n – 1)!,第一个数字确定了,剩下这些里面重新找第k个。
- n每次-1,比如第一位确定后有(n-1)!种后面数字的排法,第二位也确定了后面的数字排法就又少了一位(n – 1 – 1)!
还是不是很清楚,今晚睡觉接着想。
public String getPermutation(int n, int k) { boolean[] used = new boolean[n]; // used[i] means the digit i + 1 is used, e.g. used[0] means digit '1' is used StringBuilder sb = new StringBuilder(); int factorial = getFactorial(n - 1); int originalN = n; while (k > 0) { int index = (k - 1) / factorial; int count = 0; for (int i = 0; i < originalN; i++) { if (used[i] == false) count++; if (index == count - 1) { sb.append(i + 1); used[i] = true; break; } } n--; k = (k - 1) % factorial + 1; if (n == 0) break; factorial /= n; } return sb.toString(); } private int getFactorial(int n) { if (n == 1 || n == 0) return 1; return n * getFactorial(n - 1); }
[leetcode] Palindrome Partitioning 2 | 一个String最少切几刀能让分成的substring全是palindrome
Posted October 20, 2013
on:非常非常难,用dfs完全过不了。即使把每个substring是不是palindrome全算好了放在cache里也会挂。必须dp了。
算法:
- 首先把每个substring是不是palindrome先二维dp的做出来,参见上一题的分析。其实这一步可以合在下一步里,但是下一步本身的logic就够难想了,所以这里分开来写。
- 一维dp(为神马是一维啊!!头一次看这样的一维啊!!一维dp却是两层循环啊卧槽!!!),d[i]表示str[i..n]最少切几刀能保证substring全是palindrome。
- 把d[i]先赋成最大值。
- i从后往前,然后j在每个i处从前往后。这样做就能保证在i处,i + 1…j – 1的所有两两combination(中间的小节substring)都已经算出来了,就能用了。如果str[i, j]是个palindrome,那么可以在j 和j + 1中间切一刀,这样的#就是d[j + 1] + 1([i..j], 一刀, [j + 1..本来有几刀])。这样切法可能就比之前算的d[i]小了,于是update。
public int minCut(String s) { int n = s.length(); boolean[][] isPalin = new boolean[n][n]; for (int i = n - 1; i >= 0; i--) { // i is always <= j for (int j = i; j < n; j++) { boolean isPalindrome = s.charAt(i) == s.charAt(j) && (j - i < 2 || isPalin[i + 1][j - 1]); isPalin[i][j] = isPalindrome; } } int[] d = new int[n]; // initialize as cut between every letter for (int i = 0; i < n; i++) { d[i] = n - 1 - i; } for (int i = n - 1; i >= 0; i--) { for (int j = i; j < n; j++) { if (isPalin[i][j]) { if (j == n - 1) d[i] = 0; else d[i] = Math.min(d[i], d[j + 1] + 1); } } } return d[0]; }
- In: leetcode
- 14 Comments
这个题第二遍做code就写的挺完美的,高兴~~几个要点:
- 直接乘会溢出,所以每次都要两个single digit相乘,最大81,不会溢出。
- 比如385 * 97, 就是个位=5 * 7,十位=8 * 7 + 5 * 9 ,百位=3 * 7 + 8 * 9 …
可以每一位用一个Int表示,存在一个int[]里面。 - 这个数组最大长度是num1.len + num2.len,比如99 * 99,最大不会超过10000,所以4位就够了。
- 这种个位在后面的,不好做(10的0次方,可惜对应位的数组index不是0而是n-1),
所以干脆先把string reverse了代码就清晰好多。 - 最后结果前面的0要清掉。
public String multiply(String num1, String num2) { num1 = new StringBuilder(num1).reverse().toString(); num2 = new StringBuilder(num2).reverse().toString(); // even 99 * 99 is < 10000, so maximaly 4 digits int[] d = new int[num1.length() + num2.length()]; for (int i = 0; i < num1.length(); i++) { int a = num1.charAt(i) - '0'; for (int j = 0; j < num2.length(); j++) { int b = num2.charAt(j) - '0'; d[i + j] += a * b; } } StringBuilder sb = new StringBuilder(); for (int i = 0; i < d.length; i++) { int digit = d[i] % 10; int carry = d[i] / 10; sb.insert(0, digit); if (i < d.length - 1) d[i + 1] += carry; } //trim starting zeros while (sb.length() > 0 && sb.charAt(0) == '0') { sb.deleteCharAt(0); } return sb.length() == 0 ? "0" : sb.toString(); }
- In: leetcode
- 4 Comments
这个题挺吓人的,后来bf给讲明白了然后记得葫芦半片的,今天自己重新想了想,还是做出来了也通过了。不过是O(M * M * N的速度,没有网上说的O(M*N),忍了,反正也能pass oj.
算法:
- 先用dp求一个新矩阵,d[i][j]表示以(i, j)结尾有几个连续1(在当前row)。
- 然后遍历这个新矩阵,在每个cell,都看看“宽度是d[i][j]的矩阵最多能有多高?“,也就是往上expand到宽度变窄为止,往下expand到宽度变窄为止,然后总高度×当前宽度就是d[i][j]所属于的矩阵的最大面积。这就是个O(M * N) * O(M)。
当时给讲的时候觉得这怎么是人类能想出来的呢?然后bf说你把二维降成一维怎么做,不也是找以当前结束的1有多长吗。然后恍然大悟,这题还是有救的。题目本身写起来细节不多,不容易出bug,两遍就过了很开心~
public int maximalRectangle(char[][] matrix) { if (matrix.length == 0) return 0; int res = 0; int m = matrix.length, n = matrix[0].length; int[][] d = new int[m][n]; for (int i = 0; i < m; i++) { d[i][0] = matrix[i][0] - '0'; for (int j = 1; j < n; j++) { d[i][j] = matrix[i][j] == '1' ? d[i][j - 1] + 1 : 0; } } for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { res = Math.max(res, expand(d, i, j)); } } return res; } private int expand(int[][] d, int I, int J) { int height = 0; int width = d[I][J]; //go up for (int i = I - 1; i >= 0; i--) { if (d[i][J] >= width) { height++; } else { break; } } //go down for (int i = I; i < d.length; i++) { if (d[i][J] >= width) { height++; } else { break; } } return width * height; }
- In: leetcode
- 6 Comments
括号题也不是只用stack才能做,这题是算长度,所以stack那种一match就俩一起pop了的方式就不适合了。求极值,一维dp最好使。
- d[i]: 以i开头的最长valid parentheses有多长。
- d[i] =
- if (str[i] == ‘)’),以右括号开头必定invalid,d[i] = 0
- if (str[i] == ‘(‘),以左括号开头
- 我们想看对应的有木有右括号。因为d[i + 1]代表的sequence肯定是左括号开头右括号结尾,所以我们想catch((…))这种情况。j = i + 1 + d[i + 1],正好就是str[i]后面越过完整sequence的下一个,若是右括号,d[i] = 2 + d[i + 1]
- 除此之外,还有包起来后因为把后面的右括号用了而导致跟再往后也连起来了的情况,如((..))()()()。所以d[i]还要再加上j后面那一段的d[j + 1]
这个定义和最长公共字串那题的定义类似,都是“以某个固定位置开始/结束”。看两头的方式又像palindrome。从后往前的一维dp也不常见。挺好玩的,一下复习好多东西。
public int longestValidParentheses(String s) { if (s.length() == 0) return 0; int maxLen = 0; int[] d = new int[s.length()]; // d[i] means substring starts with i has max valid lenth of d[i] d[s.length() - 1] = 0; for (int i = s.length() - 2; i >= 0; i--) { if (s.charAt(i) == ')') d[i] = 0; else { int j = (i + 1) + d[i + 1]; if (j < s.length() && s.charAt(j) == ')') { d[i] = d[i + 1] + 2; //(()())的外包情况 if (j + 1 < s.length()) d[i] += d[j + 1];//()()的后面还有的情况 } } maxLen = Math.max(maxLen, d[i]); } return maxLen; }
—————————-用stack的做法———————————–
- stack里面装的一直是“还没配好对的那些可怜的括号的index”
- 是'(‘的时候push
- 是’)’的时候,说明可能配对了;看stack top是不是左括号,不是的话,push当前右括号
- 是的话,pop那个配对的左括号,然后update res:i和top的(最后一个配不成对的)index相减,就是i属于的这一段的当前最长。如果一pop就整个栈空了,说明前面全配好对了,那res就是最大=i+1
public int longestValidParentheses(String s) { int res = 0; Stack<Integer> stack = new Stack<Integer>(); char[] arr = s.toCharArray(); for (int i = 0; i < arr.length; i++) { if (arr[i] == ')' && !stack.isEmpty() && arr[stack.peek()] == '(') { stack.pop(); if (stack.isEmpty()) res = i + 1; else res = Math.max(res, i - stack.peek()); } else { stack.push(i); } } return res; }
这个题是非常常见的面试题,自己试着implement了两种方法,一个O(logn),一个O(n),但是写起来都不是很简洁。
O(n)的方法:binary search by start,找到一个要insert的位置(start1<start<=start2),取那个大的start2(因为它肯定会被start给replace掉,start1则不一定)。插入这个新interval,然后从头到尾检查overlap(两个参数无论谁前谁后都能用这个helper查出来),有就merge,不断吞噬overlap的。代码写起来有点长,不过思路非常清晰,不用考虑任何边界条件(什么a.start <= b.end…那些都烦死人了)。interview的时候可以写这种,因为binary search那段可能都不让你写。
还有一个trick是用iterator可以直接修改正在iterate的list,不会报modification exception,第一次用,很方便。
public ArrayList<Interval> insert(ArrayList<Interval> intervals, Interval newInterval) { int insertPosition = insertByStart(intervals, newInterval.start, 0, intervals.size() - 1); intervals.add(insertPosition, newInterval); Iterator<Interval> it = intervals.iterator(); Interval prev = it.next(); while (it.hasNext()) { Interval curr = it.next(); if (overlap(prev, curr)) { prev.start = Math.min(prev.start, curr.start); prev.end = Math.max(prev.end, curr.end); it.remove(); } else { prev = curr; } } return intervals; } private int insertByStart(ArrayList<Interval> intervals, int x, int p, int q) { if (p > q) return p; int mid = (p + q) / 2; if (intervals.get(mid).start < x) { return insertByStart(intervals, x, mid + 1, q); } else { return insertByStart(intervals, x, p, mid - 1); } } private boolean overlap(Interval a, Interval b) { return within(a.start, b) || within(a.end, b) || within(b.start, a) || within(b.end, a); } private boolean within(int v, Interval b) { return v >= b.start && v <= b.end; }
另一种方法是binary search,找出要start属于的位置,end属于的位置。但是很不好写,需要判断那些this.start >=that.end之类的,就更长了。回头看看能不能写简洁点。
- In: leetcode
- 2 Comments
这题上来没法下手,咬牙看了半天用dfs写出了个差不多的,最后卡在两个list merge上。实在看不出规律(后来发现是自己例子都写错了!!),上网一查,大神回复:
假设有n-1的答案为:G0, G1, …, Gn,想得到n的答案,只需要在G0…Gn左边加上一个0,再把G0…Gn颠倒顺序,在左边加上一个1即可。比如n=3(在分界线上倒映):
000 001 011 010 --- 110 111 101 100
用dfs可以每次求出左边是0,然后求出左边是1的,合一起就是了。两个教训:
- 这种数学题找规律,千万别自己把例子写错了。那样打死也看不出规律的。
- 拿到一题往死里想个10分钟总能想出点眉目的,别放弃。
public ArrayList<Integer> grayCode(int n) { if (n == 0) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(0); return list; } return getGrayCode(n - 1); } private ArrayList<Integer> getGrayCode(int pos) { if (pos == 0) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(0); list.add(1); return list; } ArrayList<Integer> result = new ArrayList<Integer>(); ArrayList<Integer> startsWithZero = getGrayCode(pos - 1); ArrayList<Integer> startsWithOne = new ArrayList<Integer>(); for (int i = startsWithZero.size() - 1; i >= 0; i--) { startsWithOne.add(startsWithZero.get(i) + (1 << pos)); } result.addAll(startsWithZero); result.addAll(startsWithOne); return result; }
不让用*, /, %号做整数除法。基本只能bit了。但是bit操作都是跟2有关的,所以到最后还得不断缩小范围,好能贴近结果。
算法:a / b
- a本来是想一直-b,然后减到<0了,算counter。但是这样慢,所以类似c++ vector的思路,每次发现还没减没,那减数就翻倍(b <<= 1)
- 然后到了一定程度,b实在太大了,大于a剩余的部分了,那么就停止。然后剩下的a再一点点开始减,b回归成最开始的值,重做这两步。
重点是一旦超过,b应该不要一下回到原始值,而是应该一点一点/2这样做。最刁钻的地方是test case全是各种Integer.MINVALUE之类的,搞得各种溢出,然后才发现Math.abs(MIN_VALUE)其实还是-2147483648(坑爹呢吧abs还是负数),而不是2147483648。
下面的解法是能跑过的。因为测试数据出现了-2147483648,还不能把它转成正的(2147483648就溢出了),所以直接全转换成long(64位),跑一遍就过了。
public int divide(int dividend, int divisor) { if (dividend == 0) return 0; if (divisor == 1) return dividend; //纯粹是为了防止超时 boolean positive = (dividend > 0 && divisor > 0) || (dividend < 0 && divisor < 0); long absDividend = dividend < 0 ? 0 - (long) dividend : (long) dividend; long absDivisor = divisor < 0 ? 0 - (long) divisor : (long) divisor; int result = dividePositive(absDividend, absDivisor, absDivisor); return positive ? result : 0 - result; } private int dividePositive(long p, long q, long originalDivisor) { // p / q if (p < q) return 0; //这个十分必要,否则会因为p > 0而直接下一层递归,pq永远不变,死循环了就。 int result = 0; int e = 0; while (p >= q) { //等于应该也进去。 result += 1 << e; p -= q; q = q << 1; e++; } return p > 0 ? result + dividePositive(p, originalDivisor, originalDivisor) : result; }
树的dfs变形,还是两个list来回倒。但是这题上来就写还不行,真心得在纸上画一画才能看出来规律。一开始觉得keep一个boolean,正常顺序就加后面,逆序就加前面呗,但是没注意到parent其实很可能已经不是原来顺序的了。画个四层的树就能看出来咋回事了。还有一个小bug就是最开始的boolean reverse应该是true,因为他代表了“parent下面一层要什么顺序”,而不是parent本身是什么顺序。所以还是,变量的物理意义一定要搞清再写!
public ArrayList<ArrayList<Integer>> zigzagLevelOrder(TreeNode root) {
ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
if (root == null)
return result;
ArrayList<TreeNode> parents = new ArrayList<TreeNode>();
boolean reverse = true;//first children line is reversed
parents.add(root);
while (!parents.isEmpty()) {
ArrayList<TreeNode> children = new ArrayList<TreeNode>();
for (int i = parents.size() - 1; i >= 0; i--) {
TreeNode parent = parents.get(i);
if (!reverse) {// the children list wants to be normal order
if (parent.left != null)
children.add(parent.left);
if (parent.right != null)
children.add(parent.right);
} else { //the children wants to be right to left
if (parent.right != null)
children.add(parent.right);
if (parent.left != null)
children.add(parent.left);
}
}
result.add(convertToIntegerList(parents));
reverse = !reverse;
parents = children;
}
return result;
}