Problems about removing some characters in a string to make it lexicographically smallest

There are a bunch of problems in Leetcode about removing some characters / digits in a string to make it lexicographically smallest. Here i will have a discussion about how to approach these kind of problems.

Leetcode 316. Remove Duplicate Letters
Leetcode 402. Remove K Digits
Leetcode 321. Create Maximum Number

Leetcode 316. Remove Duplicate Letters

It is possible to traverse every possible permutation of the occured characters and find the smallest one that is a subsequence of the original string, but that will takes O(N!) time which is not fast enough. We can try to construct such string from the start. Traversing characters of the string from left to right and for each character, we will need to decide whether to dump it or take it. There are two important facts to prove here:

Fact 1: If the current character has already occurred before, we should dump this current character instead of removing the previously occurred one.
Fact 2: If the current character is strictly less than its predecessor, we should remove that predecessor as long as it can occur again later.
Proof of fact 1:

Assume the current character is ‘b’, since it has already occurred before, the string can be like this:
A b C b D (A and C are substrings before and after firsts ‘b’ and D is substring after current ‘b’)
If we remove the first character ‘b’, we get A C b D
If we remove the current character ‘b’, we get A b C D
Clearly, A b C D is smaller than A C b D since the first character of C is larger than ‘b’.

Proof of fact 2:

Assume the current character is ‘b’ and its predecessor is ‘a’, the string can be like
A a b D
Removing ‘a’ we will get A b E a F since ‘a’ will occur again.
Clearly, A b E a F is smaller

class Solution {
  public String removeDuplicateLetters(String s) {
  // This array indicates the count of characters that havent' been dumped.
    int[] cnt = new int[26];
    for (char c : s.toCharArray()) {
      cnt[c - 'a']++;
    }
    // This array indicates whether a specific character has occurred before.
    boolean[] used = new boolean[26];
    LinkedList<Character> st = new LinkedList<>();
    for (char c : s.toCharArray()) {
      // According to fact 1, dump current character if it has occurred before
      if (used[c - 'a']) {
        cnt[c - 'a']--;
        continue;
      }
      /**
       * According to fact 2, cnt[st.peekLast() - 'a'] > 1 ensures the character
       * to be removed can occur again later.
       */
      while (!st.isEmpty() && st.peekLast() > c && cnt[st.peekLast() - 'a'] > 1) {
        cnt[st.peekLast() - 'a']--;
        used[st.pollLast() - 'a'] = false;
      }
      st.addLast(c);
      used[c - 'a'] = true;
    }
    StringBuilder sb = new StringBuilder();
    for (char c : st) {
      sb.append(c);
    }
    return sb.toString();
  }
}

T i m e : O ( N ) Time: O(N) Time:O(N)
S p a c e : O ( 1 ) Space:O(1) Space:O(1)

Leetcode 402. Remove K Digits

The naive solution is to traverse every possible string after removing k digits, totally ( N K N \atop K KN) possibilities which is not efficient enough.

We can still try contructing the string from start. Traversing the characters of the number string from left to right. A fact similar to fact 2 in Leetcode 316 can be useful:

Fact 1: If the current number is smaller than its predecessor, we should remove that predecessor if we havent’ already deleted K characters.

The proof of this fact is similar to that in previous section.

class Solution {
  public String removeKdigits(String num, int k) {
    LinkedList<Character> stk = new LinkedList<>();
    for (char c : num.toCharArray()) {
      // According to fact 1.
      while (!stk.isEmpty() && k > 0 && stk.peekLast() > c) {
        stk.pollLast();
        k--;
      }
      stk.addLast(c);
    }
    /**
     * It is totally possible that the characters we have visited are in non-decreasing order
     * and we haven't deleted enough characters yet. In this case, we should remove the largest
     * characters stayed in the stack, which are the k characters from the end of stack.
     */
    for (; k > 0; --k) {
      stk.pollLast();
    }
    StringBuilder sb = new StringBuilder();
    boolean hasLeading = true;
    for (char c : stk) {
      if (c == '0' && hasLeading) {
        continue;
      }
      hasLeading = false;
      sb.append(c);
    }
    return sb.length() == 0 ? "0" : sb.toString();
  }
}

T i m e : O ( N ) Time: O(N) Time:O(N)
S p a c e : O ( N ) Space:O(N) Space:O(N)

Leetcode 321. Create Maximum Number

It is almost the same as the last problem except that there are two arrays with totally K elements selected. There are K + 1 possibilities to split K elements into two arrays with the first array having L 1 ∈ [ 0 , K ] L^{}_{1} \isin \left[ {0, K} \right] L1[0,K] elements and the second array having K − L 1 K - L^{}_{1} KL1 elements.

With the same logic as the last problem, it is easy to calculate the array with L 1 L^{}_{1} L1 and K − L 1 K - L^{}_{1} KL1 elements which takes O ( m + n ) O(m + n) O(m+n) time. Then here comes the headache: How to merge two arrays into one array with the maximum number? The solution is sort of like merging two sorted arrays, in which we maintain two pointers of each array and move the pointer with larger value forward. The difference is, if two pointers holds the same value, in merging two sorted arrays problem, we can just move any pointer, but in our problem, we cannot just do that since the array is not sorted, it is necessary to keep comparing their next element, or even next next element and etc, which takes O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n)) time to know which one is larger and then move the respective pointer. So the merging part actually will take O ( ( m + n ) × m i n ( m , n ) ) O((m + n) \times min(m, n)) O((m+n)×min(m,n)) time.

class Solution {
    public int[] maxNumber(int[] nums1, int[] nums2, int k) {
        int[] ans = new int[k];
        for (int l1 = Math.max(k - nums2.length, 0); l1 <= Math.min(nums1.length, k); ++l1) {
            int l2 = k - l1;
            int[] arr1 = largestNumWithKDigits(nums1, l1);
            int[] arr2 = largestNumWithKDigits(nums2, l2);
            int[] p = merge(arr1, arr2);
            if (compareArray(p, 0, ans, 0) > 0) {
                ans = p;
            }
        }
        return ans;
    }

    private int[] largestNumWithKDigits(int[] nums, int k) {
        int[] ret = new int[nums.length];
        k = nums.length - k;
        int idx = 0;
        for (int num : nums) {
            while (idx > 0 && ret[idx - 1] < num && k > 0) {
                idx--;
                k--;
            }
            ret[idx++] = num;
        }
        for (; k > 0; --k) {
            idx--;
        }
        return Arrays.copyOfRange(ret, 0, idx);
    }

    private int[] merge(int[] p1, int[] p2) {
        int[] ret = new int[p1.length + p2.length];
        int i = 0;
        int j = 0;
        for (int idx = 0; idx < ret.length; ++idx) {
            if (j == p2.length || i < p1.length && compareArray(p1, i, p2, j) >= 0) {
                ret[idx] = p1[i++];
            } else {
                ret[idx] = p2[j++];
            }
        }
        return ret;
    }

    private int compareArray(int[] p1, int i, int[] p2, int j) {
        while (i < p1.length && j < p2.length) {
            if (p1[i] != p2[j]) {
                return p1[i] - p2[j];
            }
            i++;
            j++;
        }
        if (i < p1.length) {
            return 1;
        }
        if (j < p2.length) {
            return -1;
        }
        return 0;
    }
}

T i m e :   O ( K 2 × ( m + n ) × m i n ( m , n ) ) Time: \space O(K^{2}_{} \times (m + n) \times min(m, n)) Time: O(K2×(m+n)×min(m,n))
S p a c e :   O ( m + n ) Space: \space O(m + n) Space: O(m+n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aliengod

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值