LeetCode之動態規划
時間有限只做了下面這幾道:70、338、877、96、120、95、647,后續會繼續更新
70:爬樓梯
先來道簡單的練練手,一道經典的動態規划題目
可以采用動態規划的備忘錄法,第n節樓梯的數目等於第n-1節和n-2節的和,因為第n節一定由n-1或n-2上去的。可以不使用遞歸而使用簡單的for循環實現:
public int climbStairs(int n) {
int[] ints = new int[n + 1];
ints[1] = 1;
if (n > 1) ints[2] = 2;
for (int i = 3; i <= n; i++) {
ints[i] = ints[i - 1] + ints[i - 2];
}
return ints[n];
}
這道題的難點在於想到第n節的數目等於n-1和n-2節的數目,對於沒有接觸過動態規划的菜鳥來說還是有點意思的。
PS:如果數目過大超出了int類型的最大值,可以使用BigInteger類
338:比特位計數
這道題也還行,對二進制數來說,一個偶數的最后一位一定是0,基於這個原理可以得出下面的代碼:
for (int i = 0; i < arr.length; i++) {
if (i % 2 == 0) {
arr[i] = arr[i / 2];
}else {
arr[i] = arr[i /2] + 1;
}
}
然而~~大佬們總是有強大的方法,我那if else用一行代碼就搞定了:arr[i] = arr[i >> 1] + (i & 1)
,右移倒也還好,這個i & 1
用的就簡直了,佩服佩服。
話說這和動態規划沒什么關系吧?可我是專門挑的動態規划的題做的欸
877:石子游戲
感覺這題出的不太好,因為我想看到的是你的算法比我的算法厲害,而不是你直接給我說先手必贏,因為我完全沒有往這方面考慮過(估計大佬能想出來吧)。先手必贏的原因是先手可以決定拿所有的偶數堆還是奇數堆,而對偶數堆和奇數堆兩個一定有一個是數目比較多的(總數為奇數)
最開始我覺得比較一下前后的的大小直接挑大的就行,但是對於[3,2,10,4]這樣的排列先手會得7分而后手得12分,這顯然是錯誤的。
這一題的思路是以局部最優解得出整體最優解,典型的動態規划
public static boolean stoneGame(int[] piles) {
int length = piles.length;
//results[i][j]存儲的是piles中第i個數到第j個數組成序列的最佳拿取方式下的得分
int[][] results = new int[length][length];
//當集合中只有一個堆的時候,拿的那個人直接得分
for(int i=0;i<length;i++){
results[i][i]=piles[i];
}
//當集合中有兩個數的時候,先選的人肯定是拿較大數,分數為max-min
for(int i=0;i<length-1;i++){
results[i][i+1]=Math.abs(piles[i]-piles[i+1]);
}
for(int i=length-3;i>=0;i--){
for(int j=i+2;j<length;j++){
results[i][j]=Math.max(piles[i]-results[i+1][j],piles[j]-results[i][j-1]);
}
}
return results[0][length-1]>0;
}
對於二維數組results[i][j]
:
第一個for循環計算的是綠色的方塊,第二個for循環計算藍色的方塊,第三個填充右上的白色的方塊
最后只要看右上角的值是否大於0即可。
96:不同的二叉搜索樹
這一道題的難點在於找出節點增加時二叉搜索樹種類如何增加。
首先,新增加的節點默認是最大的(不然沒什么意義),對於n+1個節點的樹來說,有C(n+1) = C0 * Cn + C1 * C(n-1) + ... + C(n-1) * C1 + Cn * C0
其中C0 = 1, C1 = 1
解釋:對於第n+1個節點的樹來說,C0 * Cn
對應以最小節點為根節點,左節點為0個,右節點為n個;C1 * C(n-1)
對應以倒數第二小的為根節點,左邊1個節點,右邊n-1個節點......直到第n+1個節點為子節點。這樣的話代碼就很容易寫出來了。
public static int numTrees(int n) {
if(n<=1)return 1;
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
話說這道題根本和動態規划沒多大關系啊,完全是在計算CatAlan數。。。
120:三角形最小路徑和
這一題和第877題比較像,可以把三角形斜向左上,就像這樣:
解法也和877比較像:
public static int minimumTotal(List<List<Integer>> triangle) {
int[][] arr = new int[triangle.size()][triangle.size()];
List<Integer> lastList = triangle.get(triangle.size() - 1);
for (int i = 0; i < lastList.size(); i++) {
arr[arr.length - 1 - i][i] = lastList.get(i);
}
for (int n = triangle.size() -2; n >= 0; n--) {
List<Integer> list = triangle.get(n);
for (int i = 0; i <= n; i++) {
int j = n-i;
arr[i][j] = list.get(j) + Math.min(arr[i + 1][j], arr[i][j + 1]);
}
}
return arr[0][0];
}
然后再新建個一樣的二維數組記錄由該點到底部所需的最小和。
95:不同的二叉搜索樹2
這一題是第96題的增強題,不僅要求求出多少個,還要求出每個什么樣子。這一題其實最開始沒解出來,看了答案之后才動了怎么做,原來是通過暴力遞歸(貌似也只能這么做了),題目做多了之后下意識地覺得肯定不是暴力解,總想着找一種更省力的方法,然而有時候就是會陷入其中。
private static List<TreeNode> helper(int start, int end) {
List<TreeNode> res = new ArrayList<>();
if (start > end) {
res.add(null);
return res;
}
for (int val = start; val <= end; val++) {
List<TreeNode> left = helper(start, val - 1);
List<TreeNode> right = helper(val + 1, end);
for (TreeNode l : left) {
for (TreeNode r : right) {
TreeNode root = new TreeNode(val);
root.left = l;
root.right = r;
res.add(root);
}
}
}
return res;
}
647:回文子串
最開始看到這一題時想到了第五題:最長回文子串,然后想到了Manacher(馬拉車)算法,馬拉車的數組P[]既然可以求出最長回文子串,那么就能求出回文子串的個數:
//此方法對字符串進行加工
public static String preProcess(String s) {
StringBuilder sb = new StringBuilder(s.length()*2+3);
sb.append("^");
for (int i = 0; i < s.length(); i++) {
sb.append("#").append(s.charAt(i));
}
sb.append("#$");
return sb.toString();
}
// 馬拉車算法
public static int countSubstrings(String s) {
String T = preProcess(s);
// System.out.println(Arrays.toString(T.toCharArray()));
int n = T.length();
int[] P = new int[n];
int C = 0, R = 0;
for (int i = 1; i < n - 1; i++) {
int i_mirror = 2 * C - i;
if (R > i) {
P[i] = Math.min(R - i, P[i_mirror]);// 防止超出 R
} else {
P[i] = 0;// 等於 R 的情況
}
// 碰到之前講的三種情況時候,需要繼續擴展
while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])) {
P[i]++;
}
// 判斷是否需要更新 R
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
//此時P[]數組已經求出,根據該數組可以獲得回文子串的個數
int sum = 0;
for (int i = 1; i < P.length - 1; i++) {
if (i % 2 == 0) {
sum += (P[i] + 1) / 2;
} else {
sum += P[i] / 2;
}
}
// System.out.println(Arrays.toString(P));
return sum;
}
結果一看評論全都是用的兩邊掃描……我就納悶了,做第五題的時候都在馬拉車,就我兩邊掃描,結果到這題了都是兩邊掃描就我馬拉車,難道我思維比較奇葩?
貼個中心掃描的答案:
public int countSubstrings2(String s) {
for (int i=0; i < s.length(); i++){
count(s, i, i);//回文串長度為奇數
count(s, i, i+1);//回文串長度為偶數
}
return num;
}
public void count(String s, int start, int end){
while(start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)){
num++;
start--;
end++;
}
}
┓(;´_`)┏