徒手挖地球十三週目
NO.40 組合總和 II 中等
本題和徒手挖地球十二週目中組合總和的思路一樣只是少量變化,區別在於本題candidate數組中的元素不能重複使用(只能使用一次),本題數組中有重複元素。
思路一:深度優先遍歷,回溯法 從39題組合總和的基礎上進行分析改進:
- 本題數組中的每個元素不能重複使用,但是數組中存在重複元素(每個相等元素都可以使用一次)
- 每個節點的孩子應該使用下一個元素開始,即不再是index而是index+1;
- 本題數組中存在重複元素,所以僅僅採用”每個孩子從下一個元素(index+1)開始”是不夠的,因爲index之後的元素依然可能重複,因此我們不能讓相等元素不能作爲兄弟節點,但是可以作爲父子。根據這個發現,我們可以先將candidates排序,然後每次搜索時如果本節點和前面的兄弟節點相等,則剪枝。
List<List<Integer>> res=new ArrayList<>();
int[] candidates;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if (candidates==null)return res;
this.candidates=candidates;
//排序,將重複元素緊湊在一起
Arrays.sort(candidates);
dfs(target,0,new LinkedList<Integer>());
return res;
}
private void dfs(int target, int index, LinkedList<Integer> ans) {
//說明找到符合要求的路徑
if (target==0){
res.add(new ArrayList<>(ans));
return;
}
for (int i=index;i<candidates.length;i++){
//本節點和前面的兄弟節點相等,則小剪枝,跳過這條路徑
if (i > index && candidates[i] == candidates[i - 1]) {
continue;
}
//如果減數大於目標值則差爲負數,不符合結果,且後續元素都大於目標值,大剪枝,結束後序搜索
if (target<candidates[i]) {
break;
}
ans.add(candidates[i]);
//不能重複使用同一元素,所以下次搜索起點從index+1開始
dfs(target-candidates[i],i+1,ans);
//每次回溯移除最後一次添加的元素
ans.removeLast();
}
}
NO.43 字符串相乘 中等
思路一:豎式法 想一想豎式是怎麼一步一步進行的,模擬這個過程。兩個步驟:
- 逆序(從低位向高位)遍歷乘數num2的每個元素,依次與num1相乘。這個過程中需要注意除了num2的第一個元素(個位數)其他元素都需要在低位補充相應數量的0。每次相乘的結果temp是逆序的。
- 將num2的每個元素與num1相乘得到的結果temp和ans相加,此時是順序(依然是從低位到高位)遍歷兩個參數。
遍歷結束之後返回逆序結果的翻轉。
public String multiply(String num1, String num2) {
if (num1==null||num2==null||num1.equals("0")||num2.equals("0")||num1.equals("")||num2.equals(""))return "0";
StringBuilder ans=new StringBuilder();
//遍歷num2的每個元素
for (int i=0;i<num2.length();i++){
//逆序取出元素
int x = num2.charAt(num2.length()-i-1)-'0',carry=0;
StringBuilder temp=new StringBuilder();
//除了第一次個位數,其他位需要在低位補相應數量的0
for (int k=0;k<i;k++)temp.append(0);
//依次與num1的每個元素相乘
for (int j=0;j<num1.length();j++){
//逆序取出元素
int y = num1.charAt(num1.length()-j-1)-'0';
//注意加上進位值
int sum=x*y+carry;
carry=sum/10;
temp.append(sum%10);
}
//注意檢查進位值,不要遺漏
if (carry>0)temp.append(carry);
//將每位上的乘法結果和ans相加
ans=sum(ans,temp);
}
//最後需要將ans翻轉,變成正確順序
return ans.reverse().toString();
}
//將兩個字符串相加
private StringBuilder sum(StringBuilder num1, StringBuilder num2) {
StringBuilder ans=new StringBuilder();
int carry=0,len=Math.max(num1.length(),num2.length());
for (int i=0;i<len;i++){
//兩個字符串長度不相等的時候,短的那個在高位補0
int x=i<num1.length()?num1.charAt(i)-'0':0;
int y=i<num2.length()?num2.charAt(i)-'0':0;
//注意加上進位
int sum=x+y+carry;
carry=sum/10;
ans.append(sum%10);
}
//循環結束也要檢查進位,防止遺漏
if (carry>0)ans.append(carry);
return ans;
}
時間複雜度:O(MN)
思路二:優化豎式法 該算法是通過兩數相乘時,乘數某位與被乘數某位相乘,與產生結果的位置的規律來完成。具體規律如下:
-
乘數 num1 位數爲 MM,被乘數 num2 位數爲 NN, num1 x num2 結果 res 最大總位數爲 M+N。
-
num1[i] x num2[j] 的結果爲 tmp(位數爲兩位,“0x”,"xy"的形式),其第一位位於 res[i+j],第二位位於 res[i+j+1]。
public String multiply(String num1, String num2) {
if (num1==null||num2==null||num1.equals("0")||num2.equals("0")||num1.equals("")||num2.equals(""))return "0";
//兩數相乘積最多爲M+N位
int[] res=new int[num1.length()+num2.length()];
for (int i=num1.length()-1;i>=0;i--){
int x = num1.charAt(i) - '0';
for (int j=num2.length()-1;j>=0;j--){
int y = num2.charAt(j) - '0';
int sum=x*y+res[i+j+1];
res[i+j+1]=sum%10;
res[i+j]+=sum/10;
}
}
StringBuilder ans=new StringBuilder();
for (int i = 0; i < res.length; i++) {
//積的最高位可能爲零,省去不要
if (i==0&&res[0]==0)continue;
ans.append(res[i]);
}
return ans.toString();
}
時間複雜度:O(MN)
NO.46 全排列 中等
思路一:深度優先遍歷,回溯法 看到全排列,就想到DFS構建樹。重點是每條分支路徑上每個數組元素只能使用一次。可以使用一個nums.length長度的boolean類型的數組標誌每個元素的使用情況,false未使用,true已使用。
遞歸前先檢查當前元素是否被使用過,如果使用過就剪枝;如果未使用過就將當前元素加入集合並將對應的標誌設置爲true。
每次回溯的時候不僅要將最後加入集合的元素移除,還要將被移除元素對應的標誌置爲false。
List<List<Integer>> res=new ArrayList<>();
int[] nums;
public List<List<Integer>> permute(int[] nums) {
if (nums==null||nums.length==0)return res;
this.nums=nums;
//標記每個元素是否被使用過,默認值false表示未使用
boolean[] flag=new boolean[nums.length];
dfs(new LinkedList<Integer>(),flag);
return res;
}
//深度優先遍歷
private void dfs(LinkedList<Integer> combination,boolean[] flag) {
//完成組合
if (combination.size()==nums.length){
res.add(new ArrayList<>(combination));
return;
}
for (int i=0;i<nums.length;i++){
//當前元素未使用過,防止一條路徑上出現一個元素被重複使用
if (!flag[i]){
//將當前元素加入組合中,並將元素對應的標誌置爲true
combination.add(nums[i]);
flag[i]=!flag[i];
dfs(combination,flag);
//每次回溯將最後加入的元素移除,並將被移除元素對應的標誌置爲false
flag[i]=!flag[i];
combination.removeLast();
}
}
}
時間複雜度:O(N*N!)
NO.47 全排列 II 中等
思路一:深度遍歷,回溯法 本題和前文46.全排列相似,區別在於本題的數組中可能包含重複元素。
根據上一題的經驗,已經知道每一條分支路徑上每個數組元素只能使用一次,這個問題已經解決了:使用一個nums.length長度的boolean類型的數組標誌每個元素的使用情況,false未使用,true已使用。
但是僅僅依靠判斷元素的使用情況是不夠的,因爲數組中可能存在未被使用但是值相等的元素。根據前文40.組合總和II中的經驗,相等的元素不能作爲兄弟節點,但是可以作爲父子節點。於是我們就可以先對nums數組排序,再判斷每個節點使用的元素是否和之前一個兄弟節點使用的元素相等,相等則剪枝,語句形如:
//當前元素和之前一個兄弟節點使用的元素相等,且相等元素節點不是當前節點的父節點
if (i>0 && nums[i]==nums[i-1] && !nums[i-1]) continue;
爲什麼需要" &&!nums[i-1] ",以示例[1,1’,2]來說(只是簡單畫出了小部分,領會精神即可):
剪枝的地方沒什麼問題,但是[ 2,1,1’ ]這個節點使用元素" 1’ “,該節點的索引是1、且等於nums[0],如果沒有” &&!nums[i-1] “的限制也應該被剪枝。但是這個節點應該被保留,是因爲相等元素允許作爲父子節點,所以” &&!nums[i-1] "的限制是有必要的。
List<List<Integer>> res=new ArrayList<>();
int[] nums;
public List<List<Integer>> permuteUnique(int[] nums) {
if (nums==null||nums.length==0)return res;
this.nums=nums;
//對數組排序,使重複元素緊湊在一起,方便後續剪枝
Arrays.sort(nums);
//標記每個元素的使用情況,默認值false表示未使用
boolean[] flag=new boolean[nums.length];
dfs(flag,new LinkedList<Integer>());
return res;
}
private void dfs(boolean[] flag, LinkedList<Integer> track) {
//完成組合
if (track.size()==nums.length){
res.add(new ArrayList<>(new ArrayList<>(track)));
return;
}
for (int i=0;i<nums.length;i++){
//當前元素未被使用,防止一條路徑上出現一個元素被重複使用
if (!flag[i]){
//當前元素和之前一個兄弟節點使用的元素相等,且相等元素節點不是當前節點的父節點
if (i>0&&nums[i]==nums[i-1]&&!flag[i-1])continue;
//將當前元素加入組合中,並將元素對應的標誌置爲true
track.add(nums[i]);
flag[i]=true;
dfs(flag,track);
//每次回溯將最後加入的元素移除,並將被移除元素對應的標誌置爲false
track.removeLast();
flag[i]=false;
}
}
}
寫到這裏,發現很多題的時間複雜度都不會計算了。找個時間,靜下來學習整理一下遭遇過的時間複雜度計算問題。