leetCode進階算法題+解析(八十三)

在這裏要嘚瑟一下,5/25號的力扣夜貓賽,4道題都過了,第一次ak,太興奮了,感覺這一年多的付出有了收穫。從當年的完全小白到現在哪怕是運氣好但是能夠ak,也付出了很多時間精力吧。當然了一次ak只不過是加深了我的榮譽感和幸福度。我知道代表不了什麼,可能下次依然只能1,2,3題。我只是更加確信:付出終有回報,努力不會欺人。



好了,閒話少敘,繼續今天的刷題。

形成兩個異或相等數組的三元組數目

題目:給你一個整數數組 arr 。現需要從數組中取三個下標 i、j 和 k ,其中 (0 <= i < j <= k < arr.length) 。a 和 b 定義如下:
a = arr[i] ^ arr[i + 1] ^ ... ^ arr[j - 1]
b = arr[j] ^ arr[j + 1] ^ ... ^ arr[k]
注意:^ 表示 按位異或 操作。請返回能夠令 a == b 成立的三元組 (i, j , k) 的數目。

示例 1:
輸入:arr = [2,3,1,6,7]
輸出:4
解釋:滿足題意的三元組分別是 (0,1,2), (0,2,2), (2,3,4) 以及 (2,4,4)
示例 2:
輸入:arr = [1,1,1,1,1]
輸出:10
示例 3:
輸入:arr = [2,3]
輸出:0
示例 4:
輸入:arr = [1,3,5,7,9]
輸出:3
示例 5:
輸入:arr = [7,11,12,9,5,2,7,17,22]
輸出:8
提示:
1 <= arr.length <= 300
1 <= arr[i] <= 10^8

思路:這個題怎麼說呢,首先a==b。也就是a ^ b 會等於0.然後這個三元組形成的大前提就是從i到k下標異或的結果是0。而且這個數據範圍才300.我覺得暴力法就能破解。思路很清晰,我去代碼實現。
講真,這個題有毒吧?說好了的三元數組,結果兩個數居然也可以。我簡直了。。。然後用的雙層循環。我要先附上第一版代碼,雖然是錯誤的,但是原因是第一版代碼一定要三個數纔算解。其實正確答案去掉這個限制就行了:

class Solution {
    public int countTriplets(int[] arr) {
        int ans = 0;
        for(int i = 0;i<arr.length-2;i++) {
            int temp = arr[i]^arr[i+1];
            for(int j = i+2;j<arr.length;j++) {
                temp ^= arr[j];
                if(temp == 0) {
                    //起始是i,結束是k,中間任何元素都可以當j。所以中間元素數就是可能數
                    ans += (j-i);
                }
            }
        }
        return ans;
    }
}

去掉限制的正確代碼:

class Solution {
    public int countTriplets(int[] arr) {
        int ans = 0;
        for(int i = 0;i<arr.length-1;i++) {
            int temp = arr[i];
            for(int j = i+1;j<arr.length;j++) {
                temp ^= arr[j];
                if(temp == 0) {
                    //起始是i,結束是k,中間任何元素都可以當j。所以中間元素數就是可能數
                    ans += (j-i);
                }
            }
        }
        return ans;
    }
}

這個題就這樣了,比較簡單,主要是知道如何計算可能數(x-y總結果是0,那麼任何節點斷開,兩段異或都是0.所以可能數是x-y中間的元素數。所以是j-i)。這個理解了這個題就沒啥難的,下一題。

找出第K大

題目:給你一個二維矩陣 matrix 和一個整數 k ,矩陣大小爲 m x n 由非負整數組成。矩陣中座標 (a, b) 的 值 可由對所有滿足 0 <= i <= a < m 且 0 <= j <= b < n 的元素 matrix[i][j](下標從 0 開始計數)執行異或運算得到。請你找出 matrix 的所有座標中第 k 大的值(k 的值從 1 開始計數)。

示例 1:
輸入:matrix = [[5,2],[1,6]], k = 1
輸出:7
解釋:座標 (0,1) 的值是 5 XOR 2 = 7 ,爲最大的值。
示例 2:
輸入:matrix = [[5,2],[1,6]], k = 2
輸出:5
解釋:座標 (0,0) 的值是 5 = 5 ,爲第 2 大的值。
示例 3:
輸入:matrix = [[5,2],[1,6]], k = 3
輸出:4
解釋:座標 (1,0) 的值是 5 XOR 1 = 4 ,爲第 3 大的值。
示例 4:
輸入:matrix = [[5,2],[1,6]], k = 4
輸出:0
解釋:座標 (1,1) 的值是 5 XOR 2 XOR 1 XOR 6 = 0 ,爲第 4 大的值。
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 1000
0 <= matrix[i][j] <= 106

1 <= k <= m * n

思路:這個題咋說呢,讀了好幾遍題目才懂了題意。我的想法是用一個二維數組壓縮。第一次記錄每一列/行的異或結果。然後第二遍遍歷記錄每一行的異或結果。這樣就獲取所有可能的a,b的結果了。有點類似dp。然後排序,獲取第k個值。思路差不多是這樣,我去實現下試試。
第一版代碼:

class Solution {
    public int kthLargestValue(int[][] matrix, int k) {
        int m = matrix.length;
        int n = matrix[0].length;
        int[][] dp = new int[m + 1][n + 1];
        List<Integer> ans = new ArrayList<Integer>();
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                dp[i][j] = dp[i - 1][j] ^ dp[i][j - 1] ^ dp[i - 1][j - 1] ^ matrix[i - 1][j - 1];
                ans.add(dp[i][j]);
            }
        }
        ans.sort((i1,i2)->{return i2-i1;});
        return ans.get(k - 1);
    }
    
}

做的時候我還覺得我用到了dp,以爲能不錯。結果發現ac是ac了。但是性能低空略過,然後我覺得是m*n的時間複雜度是不可能再少了的。能優化的點也就是排序這塊了。。然而我沒啥思路。所以直接去看性能第一的代碼吧:

class Solution {
    public int kthLargestValue(int[][] matrix, int k) {
        final int N = (matrix == null) ? 0 : matrix.length;
        final int M = (N == 0) ? 0 : matrix[0].length;
        int[][] xor = new int[N + 1][M + 1];
        for (int i = 0; i < N; ++i) {
            for (int j = 0; j < M; ++j) {
                xor[i + 1][j + 1] = matrix[i][j] ^ xor[i][j + 1] ^ xor[i + 1][j] ^ xor[i][j];
            }
        }
        int idx = 0;
        int[] nums = new int[N * M];
        for (int i = 1; i <= N; ++i) {
            for (int j = 1; j <= M; ++j) {
                nums[idx++] = xor[i][j];
            }
        }
        return quickSelect(nums, 0, nums.length - 1, nums.length - k + 1);
    }

    private int quickSelect(int[] nums, int start, int end, int k) {
        if (start == end) {
            return nums[start];
        }
        int left = start;
        int right = end;
        int pivot = nums[start + (end - start) / 2];
        while (left <= right) {
            if (nums[left] < pivot) {
                left++;
            } else if (nums[right] > pivot) {
                right--;
            } else {
                int temp = nums[left];
                nums[left++] = nums[right];
                nums[right--] = temp;
            }
        }
        if (start + k - 1 <= right) {
            return quickSelect(nums, start, right, k);
        }
        if (start + k - 1 >= left) {
            return quickSelect(nums, left, end, start + k - left);
        }
        return nums[right + 1];
    }
}

思路沒啥問題,優化點也確實在排序上。我用的list。這裏用了數組來存儲。然後用了一個方法來獲取第k個元素的值。重點應該是在這個排序上吧。反正這個題就這樣了,下一題。

根據前序和後序遍歷構造二叉樹

題目:返回與給定的前序和後序遍歷匹配的任何二叉樹。pre 和 post 遍歷中的值是不同的正整數。

示例:
輸入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
輸出:[1,2,3,4,5,6,7]
提示:
1 <= pre.length == post.length <= 30
pre[] 和 post[] 都是 1, 2, ..., pre.length 的排列
每個輸入保證至少有一個答案。如果有多個答案,可以返回其中一個。

思路:這個題咋說呢,但凡帶個中序就能確定一棵樹了。問題是隻有前序後序排列。所以其實應該是無法確定一棵樹了。這裏簡單說下前序中序後序的順序:前序:根,左,右。 中序:左,根,右。後序:左,右,根。然後現在給定是是前序和後序。所以知道了根左右和左右根。但是之所以說前後確定不了唯一的一棵樹的原因也是這:左右子樹的界限是不清楚的。繼續說這個題目:因爲左右節點的順序是不會變的。不管是根左右還是左右根,起碼都保證了左右節點的順序不變。而且根的位置是從前到後了。所以我們可以判斷當前pre中當前節點和post中節點的位置。如果說這個元素位置相對往後移動了。說明這個節點是一個根節點。然後如果是子節點上一層根節點,則只會往後移動兩步。注意我說的不是相對位置。是指原本在他之後,結果在他之前的元素。比如上面demo中的本來是2,4,5.後序變成4.5,2.也就是兩個元素提前面去了。同理1也是這樣的。本來後面2-7.結果因爲上述第二次,所以2乘3個元素到前面了。當然我這個思路是要都是完整的二叉樹。我慢慢去代碼試試吧。
第一版代碼:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode constructFromPrePost(int[] pre, int[] post) {
        return dfs(pre,post);
    }
    public TreeNode dfs(int[] pre, int[] post) {
        int len = pre.length;
        if(len == 0) return null;
        // 根節點比較好確定。前序第一個和後序最後一個。
        TreeNode ans = new TreeNode(pre[0]);
        if (len == 1) return ans;
        // 前序第二個元素是左子樹根節點。後序倒數第二個元素是右子樹根節點。如果不一樣則遞歸分樹
        if (pre[1] != post[post.length-2]) {//Arrays.copyOfRange(data,  2 ,  7 );
            int v = pre[1];
            int idx = -1;//記錄左子樹根節點在後序中的位置。
            for(int i = 0;i<post.length;i++) if(post[i] == v) idx = i;
            //左右子樹都不包含當前根節點,也就是pre[0]和post[len-1]
            int[] post1 = Arrays.copyOfRange(post, 0, idx+1);
            int[] post2 = Arrays.copyOfRange(post, idx+1, len-1);
            int[] pre1 = Arrays.copyOfRange(pre,1,1+post1.length);
            int[] pre2 = Arrays.copyOfRange(pre,post1.length+1,len);
            ans.left = dfs(pre1, post1);
            ans.right = dfs(pre2, post2);
        }else {//說明當前樹只有一個根。左右無所謂了
            ans.left = dfs(Arrays.copyOfRange(pre,1,len), Arrays.copyOfRange(post, 0, len-1));
        }
        return ans;
    }
}

咳咳,和之前的思路不能說一模一樣,只能說幾乎不同。。。因爲我在分解左右子樹的時候就發現,其實這個題可以轉化成遞歸問題。然後就和之前的思路完全不同了。。。反正最終變成了上述的代碼。過是過了,畢竟節點30個以內。不過我覺得優化空間還是蠻大的。比如說複製數組可以換成傳起始下標?不過這個太複雜了懶得自己去想了,我直接去看看性能第一的代碼吧:
開過光的嘴預測到了答案:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode constructFromPrePost(int[] pre, int[] post) {
        return helper(pre,post,0,pre.length-1,0,post.length-1);
    }
    public TreeNode helper(int[] pre,int[] post,int prestart,int preend,int poststart,int postend){
        if(prestart>preend||poststart>postend)return null;
        TreeNode root=new TreeNode(pre[prestart]);
        if (prestart == preend)
            return root;
        int index=0;
        while(post[index]!=pre[prestart+1]){
            index++;
        }
        root.left=helper(pre,post,prestart+1,prestart+1+index-poststart,poststart,index);
        root.right=helper(pre,post,prestart+2+index-poststart,preend,index+1,preend-1);
        return root;
        
    }
}

果然是不復制數組,而是直接用下標表示數值的方法,性能超過了百分百。其實不過是2ms進化到1ms而已。總而言之二叉樹的題目,第一反應遞歸一般不會出錯。這個題就這樣了,下一題。

不相交的線

題目:在兩條獨立的水平線上按給定的順序寫下 nums1 和 nums2 中的整數。現在,可以繪製一些連接兩個數字 nums1[i] 和 nums2[j] 的直線,這些直線需要同時滿足滿足: nums1[i] == nums2[j]
且繪製的直線不與任何其他連線(非水平線)相交。
請注意,連線即使在端點也不能相交:每個數字只能屬於一條連線。以這種方法繪製線條,並返回可以繪製的最大連線數。

示例 1:
輸入:nums1 = [1,4,2], nums2 = [1,2,4]
輸出:2
解釋:可以畫出兩條不交叉的線,如上圖所示。
但無法畫出第三條不相交的直線,因爲從 nums1[1]=4 到 nums2[2]=4 的直線將與從 nums1[2]=2 到 nums2[1]=2 的直線相交。
示例 2:
輸入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
輸出:3
示例 3:
輸入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
輸出:2
提示:
1 <= nums1.length <= 500
1 <= nums2.length <= 500
1 <= nums1[i], nums2[i] <= 2000

思路:不知道爲啥審完題我第一反應又是dp,我這怕不是dp綜合徵吧。題目標籤是數組。但是我還是覺得可以用dp來做。每一對元素可以分爲連和不連。如果連則取下面的下標往下遍歷。如果不連則從當前遍歷。兩個遊標記錄上下的線當前指向。思路還算清晰,不太好用言語表達出來,我去實現下試試。
真的要代碼實現之前,我發現了個問題:其實這個問題可以轉換思路,變成nums1和nums2的最長子序列。因爲可以按照子序列的順序連線。肯定不存在交叉。然後這個題就做過好幾遍了,我覺得我思路沒問題,我去寫代碼。
第一版代碼:

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length+1][nums2.length+1];
        for(int i = 0;i<nums1.length;i++) {
            for(int j = 0;j<nums2.length;j++) {
                if(nums1[i] == nums2[j]) {
                    dp[i+1][j+1] = dp[i][j]+1;
                }else {
                    dp[i+1][j+1] = Math.max(dp[i][j+1], dp[i+1][j]);
                }
            }
        }
        return dp[nums1.length][nums2.length];
    }
}

ac是ac了,性能沒眼看了。。但是別的不說思路起碼是對的。然後這個子數組的優化沒啥好說的,畢竟我的技術最多也就能壓縮下空間。時間複雜度少不了。。我直接去看看性能第一的代碼吧:
!!!!!我踏馬簡直了,一樣的思路壓縮了下空間,性能就上來了。。一點脾氣沒有了,附上代碼:

class Solution {
    public int maxUncrossedLines(int[] A, int[] B) {
        int lena = A.length, lenb = B.length;
        int[] dp = new int[lenb+1];
        for(int i = 1;i<=lena;i++) {
            int[] temp = new int[lenb+1];
            for(int j = 1;j<=lenb;j++) {
                if(A[i-1]==B[j-1]) {
                    temp[j] = dp[j-1]+1;
                }
                else {
                    temp[j] = Math.max(dp[j], temp[j-1]);
                }
            }
            dp = temp;
        }
        return dp[lenb];
    }
}

因爲當前數值只和上一層有關,所以壓縮成兩個數組很容易想到,我是沒想到性能差別這麼大,哎,這個題就這樣吧,下一題。

查找和替換模式

題目:你有一個單詞列表 words 和一個模式 pattern,你想知道 words 中的哪些單詞與模式匹配。如果存在字母的排列 p ,使得將模式中的每個字母 x 替換爲 p(x) 之後,我們就得到了所需的單詞,那麼單詞與模式是匹配的。(回想一下,字母的排列是從字母到字母的雙射:每個字母映射到另一個字母,沒有兩個字母映射到同一個字母。)返回 words 中與給定模式匹配的單詞列表。你可以按任何順序返回答案。

示例:
輸入:words = ["abc","deq","mee","aqq","dkd","ccc"], pattern = "abb"
輸出:["mee","aqq"]
解釋:
"mee" 與模式匹配,因爲存在排列 {a -> m, b -> e, ...}。
"ccc" 與模式不匹配,因爲 {a -> c, b -> c, ...} 不是排列。
因爲 a 和 b 映射到同一個字母。
提示:
1 <= words.length <= 50
1 <= pattern.length = words[i].length <= 20

思路:這個題一開始看題目沒太明白,但是看了示例秒懂。有點類似中文的按照格式寫成語,ABAB,AABB,ABBA之類的模板。只不過這個題的模板是pattern,然後我們照着這個模板去套單詞就行了。感覺挺好實現的。因爲單詞數不超過50.而且長度不超過20。所以我覺得暴力應該可能可以。這個題咋說呢,在我看來是怎麼都能解。但是怎麼能解的好又沒啥思路。
第一版本代碼:

class Solution {
    String[] c;
    public List<String> findAndReplacePattern(String[] words, String pattern) {
        c = new String[] {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T"};
        pattern = getStr(pattern);
        List<String> ans = new ArrayList<String>();
        for(String s:words) if(getStr(s).equals(pattern)) ans.add(s);
        return ans;
    }
    public String getStr(String str) {
        int temp = 0;
        for(int i = 0;i<str.length();i++) {
            char cur = str.charAt(i);
            if(cur<97) continue;//這個元素已經替換過了,直接下一個
            str = str.replaceAll(cur+"", c[temp++]);
        }
        return str;
    }
}

仗着數據少就硬生生的暴力,我覺得20,50的數據範圍絕對不會超時,果然ac了,但是性能只超過了百分之7。。emmmmm....其實別的實現方法也有好多,如說記錄出現的下標,然後下標比對什麼的。再來一種實現方式吧:

class Solution {
    public List<String> findAndReplacePattern(String[] words, String pattern) {
        int[] temp = getStr(pattern);
        List<String> ans = new ArrayList<String>();
        for(String s:words) {
            int[] cur = getStr(s);
            boolean flag = true;
            for(int i = 0;i<cur.length;i++) {
                if(cur[i] != temp[i]) {
                    flag = false;
                    break;
                }
            }
            if(flag) ans.add(s);
        }
        return ans;
    }
    public int[] getStr(String str) {
        int[] c = new int[str.length()];
        char[] s = str.toCharArray();
        int temp = 1;
        for(int i = 0;i<s.length;i++) {
            if(c[i] != 0) continue;
            c[i] = temp++;
            for(int j = i+1;j<s.length;j++) {
                if(c[j] != 0) continue;
                if(s[j] == s[i]) c[j] = c[i];
            }
        }
        return c;
    }
}

改了下思路,直接用染色的思路實現了,去掉了來回來去字符串操作,性能一下子就上來了。雖然實際上代碼看着多了。但是這種實現比較好理解。思路很簡單,一個元素代表一個數字。從前往後出現的依次遞加。這樣哪怕不是一樣的字符但是格式一樣數組也應該一樣。
本篇筆記就記到這裏了,如果稍微幫到你了記得點個喜歡點個關注,也祝大家工作順順利利,生活健健康康,週末愉快喲~!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章