徒手挖地球十三周目

徒手挖地球十三周目

NO.40 组合总和 II 中等

在这里插入图片描述

本题和徒手挖地球十二周目组合总和的思路一样只是少量变化,区别在于本题candidate数组中的元素不能重复使用(只能使用一次),本题数组中有重复元素。

思路一:深度优先遍历,回溯法 从39题组合总和的基础上进行分析改进:

  1. 本题数组中的每个元素不能重复使用,但是数组中存在重复元素(每个相等元素都可以使用一次)
  2. 每个节点的孩子应该使用下一个元素开始,即不再是index而是index+1;
  3. 本题数组中存在重复元素,所以仅仅采用”每个孩子从下一个元素(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 字符串相乘 中等

10uteS.png

思路一:竖式法 想一想竖式是怎么一步一步进行的,模拟这个过程。两个步骤:

  1. 逆序(从低位向高位)遍历乘数num2的每个元素,依次与num1相乘。这个过程中需要注意除了num2的第一个元素(个位数)其他元素都需要在低位补充相应数量的0。每次相乘的结果temp是逆序的。
  2. 将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)

思路二:优化竖式法 该算法是通过两数相乘时,乘数某位与被乘数某位相乘,与产生结果的位置的规律来完成。具体规律如下:

  1. 乘数 num1 位数为 MM,被乘数 num2 位数为 NN, num1 x num2 结果 res 最大总位数为 M+N。

  2. num1[i] x num2[j] 的结果为 tmp(位数为两位,“0x”,"xy"的形式),其第一位位于 res[i+j],第二位位于 res[i+j+1]。

166SQe.png

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 中等

10yScq.png

思路一:深度遍历,回溯法 本题和前文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;
        }
    }
}

写到这里,发现很多题的时间复杂度都不会计算了。找个时间,静下来学习整理一下遭遇过的时间复杂度计算问题。

发布了44 篇原创文章 · 获赞 22 · 访问量 1923

猜你喜欢

转载自blog.csdn.net/qq_42758551/article/details/104200425