String 构造式 DFS

完成:11/8

“字符串构造式 DFS + backtracking”,还有最简单直接又好分析时间复杂度的做法,就是从 String 中 每个字符的角度出发,去考虑 “拿 / 不拿”

StringBuilder 的构造式 DFS + Backtracking,对 “加 / 不加” 的先后顺序是敏感的, 都是 先走“不加”的分支 再走“加”的分支

原因在于,这个模板是 “带着当前元素考虑” 的 DFS,在当前函数尾部回溯。当两个 dfs 同处一层,而同层 dfs 只在最尾部才回溯的时候,如果先跑添加的分支,会在后面执行不添加分支时候出错。

否则就一次 dfs 后面跟着一次 sb.setLength(),也可以。

Generalized Abbreviation

public class Solution {
    public List<String> generateAbbreviations(String word) {
        List<String> result = new ArrayList<>();
        dfs(word.toCharArray(), result, new StringBuilder(), 0, 0);
        return result;
    }

    private void dfs(char[] word, List<String> result, StringBuilder sb, int index, int num) {
        int len = sb.length();
        if(index == word.length) {
            if(num != 0) sb.append(num);
            result.add(sb.toString());
        } else {
            // 不拿
            dfs(word, result, sb, index + 1, num + 1);
            // 拿
            if(num != 0) sb.append(num + "");
            dfs(word, result, sb.append(word[index]), index + 1, 0);
        }
        // backtracking
        sb.setLength(len);
    } 
}

Remove Invalid Parentheses

考点: 用什么方法做: 先扫一遍原 string ,记录下需要移除的左括号数量和右括号数量,保证最后的解最优;

描述算法过程: 于是开始 DFS,对于每一个新的字符,我们都有两个选择:'留' 或者 '不留',就像二叉树的分叉一样,留下了 dfs + backtracking 的空间。

于是当我们针对各种情况进行 dfs 的时候,我们一定可以考虑到所有可能的有效 path,接下来需要定义什么是 “有效 path”, 就是open == 0.

怎么样保证左右括号的匹配,用什么数据结构: 两个variable保存

如何保证最优解: prune操作, 如果在任何时刻,左右括号和开括号的数量为负,我们都可以直接剪枝返回。

O(2^n)

可能的问题: open 变量有什么用?

public class Solution {
    public List<String> removeInvalidParentheses(String s) {
        Set<String> set = new HashSet<>();

        int rmL = 0;
        int rmR = 0;

        // count how many parenthese need to be removed
        for(int i = 0; i < s.length(); i++) {
            if(s.charAt(i) == '(') rmL++;

            if(s.charAt(i) == ')') {
                if(rmL != 0) rmL--;
                else rmR++;
            }
        }
        dfs(s, set, 0, rmL, rmR, 0, new StringBuilder());

        return new ArrayList<String>(set);
    }

    public void dfs(String s, Set<String> set, int i, int rmL, int rmR, int open, StringBuilder sb) {
        // base case 是左右括号和开括号数量都为零,并且 index 读取完了所有字符,则把结果添加进去;
        if(i == s.length() && rmL == 0 && rmR == 0 && open == 0) {
            set.add(sb.toString());
            return;
        }

        // exit condition
        if(i >= s.length() || rmL < 0 || rmR < 0 || open < 0) {
            return;
        }

        char c = s.charAt(i++);
        // mark
        int len = sb.length();

        if(c == '(') {
            // drop it
            dfs(s,set,i,rmL - 1,rmR, open, sb);
            // add it
            dfs(s,set,i,rmL,rmR,open + 1, sb.append(c));
        } else if(c == ')') {
            // drop 
            dfs(s,set,i,rmL,rmR - 1,open,sb);
            // add
            dfs(s,set,i,rmL,rmR,open - 1,sb.append(c));
        } else {
            // add it 
            dfs(s,set,i,rmL,rmR,open,sb.append(c));
        }
        // backtracking
        sb.setLength(len);
    }
}

Expression Add Operators

难题,很少会考察到,重点是思维的训练,不要求bug-free的写出来。

在每一个可能加入运算符的位置上,逐个尝试乘法,加法和减法。

注意点: 为了避免取到000这样的数,如果 start 位置是 0 而且当前循环的 index 还是 0,直接 return

流程:

  • dfs + backtracking 思路
  • dfs 函数中存 "左面表达式的当前值" 和 "上一步操作的结果" (用于处理优先级)
  • 参数都用 Long,防止溢出,毕竟 String 可能是个大数
  • start == 0 的时候,直接考虑所有可能的起始数字扔进去,同时更新 cumuVal 和 prevVal;
  • 除此之外,依次遍历所有可能的 curVal parse,按三种不同的可能操作做 dfs + backtracking;
    • 加法:直接算,prevVal 参数传 curVal,代表上一步是加法;
    • 减法:直接算,prevVal 参数传 -curVal,代表上一步是减法;
    • 乘法:要先减去 prevVal 抹去上一步的计算,然后加上 prevVal, curVal 代表当前值;同时传的 preVal 参数也等于 prevVal curVal.
    • 乘法操作这种处理方式,在遇到连续乘法的时候可以看到是叠加的;但是前一步操作如果不是乘法,则可以优先计算乘法操作。
public class Solution {

    List<String> res = new ArrayList();

    public List<String> addOperators(String num, int target) {
        helper(num, target, "", 0, 0);
        return res;
    }

    private void helper(String num, int target, String tmp, long currRes, long prevNum){
        // 如果计算结果等于目标值,且所有数都用完了,则是有效结果
        if(currRes == target && num.length() == 0){
            res.add(exp);
            return;
        }
        // 搜索所有可能的拆分情况
        for(int i = 1; i <= num.length(); i++){
            String currStr = num.substring(0, i);
            // 对于前导为0的数予以排除
            if(currStr.length() > 1 && currStr.charAt(0) == '0'){
                // 这里是return不是continue
                return;
            }
            // 得到当前截出的数
            long currNum = Long.parseLong(currStr);
            // 去掉当前的数,得到下一轮搜索用的字符串
            String next = num.substring(i);
            // 如果不是第一个字母时,可以加运算符,否则只加数字
            if(tmp.length() != 0){
                // 乘法
                helper(next, target, tmp+"*"+currNum, (currRes - prevNum) + prevNum * currNum, prevNum * currNum);
                // 加法
                helper(next, target, tmp+"+"+currNum, currRes + currNum, currNum);
                // 减法
                helper(next, target, tmp+"-"+currNum, currRes - currNum, -currNum); 
            } else {
                // 第一个数
                helper(next, target, currStr, currNum, currNum);
            }
        }
    }
}

results matching ""

    No results matching ""