leetcode(10) || 动态规划 && 位操作

目录

动态规划

Triangle(mid)

Maximum Subarray(easy)

Maximum Product Subarray(mid)

Longest Increasing Subsequence(最长递增序列)(mid)

Palindrome Partitioning II(分割回文串)(hard)

Maximal Rectangle(最大矩形)(hard)

Best Time to Buy and Sell Stock III(hard)

Best Time to Buy and Sell Stock with Cooldown(hard)

Minimum Path Sum(mid)

Interleaving String(hard)

Scramble String

Decode Ways(mid)

Distinct Subsequences(hard)

Word Break(mid)

Word Break II(hard)

Dungeon Game

House Robber(打家劫舍)(easy)

House Robber II(mid)

House Robber III(hard)

Range Sum Query - Immutable(easy)

range Sum Query 2D - Immutable(medium)

位操作

Number of 1 Bits(easy)

Reverse Bits(easy)

Repeated DNA Sequences(medium)

Gray Code

Single Number(easy)

Single Number II(mid)

Single Number III(mid)

Power of Two

Missing Number(easy)

Maximum Product of Word Lengths(mid)

Bitwise AND of Numbers Range

Power of Three(easy)


动态规划

注意点:

对于一部分的计算,可以把已经算过的节点用哈希表保存起来,以后递归调用的时候,现在哈希表里找,如:Word Break II,House Robber III

 

Triangle(mid)

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

For example, given the following triangle

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

Note:

Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.

解法一:从上往下

不新建dp数组,而是直接复用triangle数组,我们希望一层一层的累加下来,从而使得 triangle[i][j] 是从最顶层到 (i, j) 位置的最小路径和,那么我们如何得到状态转移方程呢?其实也不难,因为每个结点能往下走的只有跟它相邻的两个数字,那么每个位置 (i, j) 也就只能从上层跟它相邻的两个位置过来,也就是 (i-1, j-1) 和 (i-1, j) 这两个位置,那么状态转移方程为:

triangle[i][j] = min(triangle[i - 1][j - 1], triangle[i - 1][j])

我们从第二行开始更新,注意两边的数字直接赋值上一行的边界值,那么最终我们只要在最底层找出值最小的数字,就是全局最小的路径和啦

这种方法可以通过OJ,但是毕竟修改了原始数组triangle,并不是很理想的方法。

class Solution {
public:
    int minimumTotal(vector>& triangle) {
        if(triangle.empty()) return 0;
        int h = triangle.size(),res = INT_MAX;
        for(int i = 1;i < h;i++){
            int len = triangle[i].size();
            triangle[i][0] += triangle[i-1][0];
            triangle[i][len-1] += triangle[i-1][len-2];
            for(int j = 1;j < len-1;j++){
                triangle[i][j] += min(triangle[i-1][j-1],triangle[i-1][j]);
            }
        }
        for(int i = 0;i < triangle[h-1].size();i++){
            res = min(res,triangle[h-1][i]);
        }
        return res;
    }
};

解法二:从下往上

这道题要求的是一个从上往下和最小的路径,下一步只能从下一行相邻的数中找。 很显然,从上往下找路径的可能是不断增加的,因此我们考虑从下往上找。 
(1)很显然,当只有一行时: 
最小值为那一行最小的数。 
(2)当有两行时: 
最小值为min(第二行中的两个数)+第一行的数。 
依次类推,我们从下往上,可以找到某一行某个数到最后一行相应的最小和,即: 
result[j] = min(result[j], result[j + 1]) + triangle[size - 2 - i][j]; 
当循环都第一行时,则是我们所求的从上往下路径的最小和。 

class Solution {
public:
    int minimumTotal(vector>& triangle) {
        if(triangle.empty()) return 0;
        int h = triangle.size()-1;
        vector res(triangle[h]);
        for(int i = h-1;i >= 0;i--){
            for(int j = 0;j < triangle[i].size();j++){
                res[j] = min(res[j],res[j+1]) + triangle[i][j];
            }
        }
        return res[0];
    }
};


Maximum Subarray(easy)

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

Example:

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Follow up:

If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.

思路:这题挺简单,贪心即可

class Solution {
public:
    int maxSubArray(vector& nums) {
        if(nums.empty()) return 0;
        int front = nums[0],max_num = front;
        for(int i = 1;i < nums.size();i++){
            if(front > 0) front += nums[i];
            else front = nums[i];
            max_num = max(front,max_num);
        }
        return max_num;
    }
};

解法二:分治

分治法的基本思想是将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小,递归地求解出子问题,如果子问题的规模足够小,则停止递归,直接求解,最后将子问题的解组合成原问题的解。
  对于最大子数组,我们要寻求子数组A[low...high]的最大子数组。令mid为该子数组的中央位置,我们考虑求解两个子数组A[low...mid]和A[mid+1...high]。A[low...high]的任何连续子数组A[i...j]所处的位置必然是以下三种情况之一:

  1. 完全位于子数组A[low...mid]中,因此low≤i≤j≤mid.low≤i≤j≤mid.
  2. 完全位于子数组A[mid+1...high]中,因此mid
  3. 跨越了中点,因此low≤i≤mid

因此,最大子数组必定为上述3种情况中的最大者。对于情形1和情形2,可以递归地求解,剩下的就是寻找跨越中点的最大子数组。
  任何跨越中点的子数组都是由两个子数组A[i...mid]和A[mid+1...j]组成,其中low≤i≤midlow≤i≤mid且mid

class Solution {
public:
    int maxSubArray(vector& nums) {
        return helper(nums,0,nums.size()-1);
    }
    
    int helper(vector& nums,int l,int r){
        if(l >= r) return nums[l];
        int mid = (l+r)/2;
        // 左边的最大值
        int l_max = helper(nums,l,mid-1);
        // 右边
        int r_max = helper(nums,mid+1,r);
        // 跨越中点的最大值
        int m_max = nums[mid],cur = m_max;
        for(int i = mid-1;i >= l;i--){
            cur += nums[i];
            m_max = max(m_max,cur);
        }
        cur = m_max;
        for(int i = mid+1;i <= r;i++){
            cur += nums[i];
            m_max = max(m_max,cur);
        }
        return max(m_max,max(l_max,r_max));
    }
};

 

Maximum Product Subarray(mid)

Given an integer array nums, find the contiguous subarray within an array (containing at least one number) which has the largest product.

Example 1:

Input: [2,3,-2,4]
Output: 6

Explanation: [2,3] has the largest product 6.

Example 2:

Input: [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

题意:注意,这里单个数也算product,所以[-4,2]的结果是2

解法一:和上一题解法一思路差不多

不同的是这里需要维护一个正数最大值和一个负数最小值,但是这个做法有个局限就是,当

class Solution {
public:
    int maxProduct(vector& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        if(n == 1) return nums[0];
        
        int pos = nums[0],neg;
        if(pos >= 0) neg = 0;
        else {neg = pos;pos = 0;}
        int max_num = pos;
        for(int i = 1;i < n;i++){
            pos *= nums[i];
            neg *= nums[i];
            if(pos < 0 || neg > 0) swap(pos,neg);
            if(pos == 0 && nums[i] > 0) pos = nums[i];
            else if(neg == 0 && nums[i] < 0) neg = nums[i];
            max_num = max(pos,max_num);
        }
        return max_num;
    }
};

一个更加简洁的思路:

1. 当遍历到一个正数时,此时的最大值等于之前的最大值乘以这个正数和当前正数中的较大值,此时的最小值等于之前的最小值乘以这个正数和当前正数中的较小值。

2. 当遍历到一个负数时,我们先用一个变量t保存之前的最大值mx,然后此时的最大值等于之前最小值乘以这个负数和当前负数中的较大值,此时的最小值等于之前保存的最大值t乘以这个负数和当前负数中的较小值。

3. 在每遍历完一个数时,都要更新最终的最大值。

class Solution {
public:
    int maxProduct(vector& nums) {
        int n = nums.size();
        int front_max = nums[0],front_min = nums[0];
        int max_num = front_max;
        for(int i = 1;i < n;i++){
            // 注意这里要存,否则直接用front_max,front_min会在front_max改变后去计算front_min引起错误!!!!!!
            int fmax = front_max,fmin = front_min;
            front_max = max(fmax*nums[i],max(fmin*nums[i],nums[i]));
            front_min = min(fmax*nums[i],min(fmin*nums[i],nums[i]));
            max_num = max(front_max,max_num);
        }
        return max_num;
    }
};

 

Longest Increasing Subsequence(最长递增序列)(mid)

Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

Note:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n*n) complexity.

Follow up: Could you improve it to O(n log n) time complexity?

解法1:O(n*n)

LIS算是动态规划中的经典题目,DP中求什么就设什么。我们设dp[i]是在0~i上的LIS的长度。我们现在需要考虑初始条件和转移方程。

(1)初始条件:
在最开始时,dp[i] = 1代表序列中只含有自身。

(2)转移方程:
本问题的关键在于对于循环和大小关系的确定:我们对于任意的一个i,需要从0~i-1判断每个nums[i],nums[j]的大小关系。从而确定dp[i]的值,通过两者之间的大小关系我们可以得出: 
若nums[i] > nums[j],则dp[i] = dp[j]+1 
然后对于j从0~i-1,我们对上述式子求最大值,便可得出转移方程如下: 
dp[i] = max(dp[i],dp[j]+1)
 

class Solution {
public:
    int lengthOfLIS(vector& nums) {
        int n = nums.size();
        if(n < 2) return n;
        vector dp(n,1);
        int max_num = 1;
        for(int i = 1;i < n;i++){
            for(int j = 0;j < i;j++){
                if(nums[i] > nums[j]) dp[i] = max(dp[i],dp[j]+1);
            }
            max_num = max(max_num,dp[i]);
        }
        return max_num;
    }
};

 

解法二:O(NlogN)

dp数组size为nums.size+1,里面用来存放长度 i 序列最小末尾的数,而后我们只需每次二分查找更新dp即可 。

例:

假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。

我们定义一个序列B[1..9],然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN),于是算法的时间复杂度就降低到了O(NlogN)!

class Solution {
public:
    int lengthOfLIS(vector& nums) {
        int n = nums.size();
        if(n < 2) return n;
        vector dp(n+1,0);
        dp[1] = nums[0];
        int max_num = 1;
        for(int i = 1;i < n;i++){
            max_num = binarySearch(dp,nums[i],1,max_num);
        }
        return max_num;
    }
    
    int binarySearch(vector &dp,int num,int start,int end){
        // 注意这里对原始end的保留,之后返回
        int ori_end = end;
        if(dp[end] < num) {dp[end+1] = num; return end+1;}
        else if(dp[start] > num) {dp[start] = num; return end;}
        while(start <= end){
            int mid = (start+end)/2;
            if(dp[mid] == num) return ori_end;
            else if(dp[mid] < num){
                if(mid < end && dp[mid+1] > num) {dp[mid+1] = num; return ori_end;}
                else start = mid+1;
            }
            else{
                if(mid > start && dp[mid-1] < num) {dp[mid] = num; return ori_end;}
                else end = mid-1;
            }
        }
        return ori_end;
    }
};

 

Palindrome Partitioning II(分割回文串)(hard)

Given a string s, partition s such that every substring of the partition is a palindrome.

Return the minimum cuts needed for a palindrome partitioning of s.

Example:

Input: "aab"
Output: 1
Explanation: The palindrome partitioning ["aa","b"] could be produced using 1 cut.

重述题意:输入一个字符串,将其进行分割,分割后各个子串必须是“回文”结构,要求最少的分割次数。显然,为了求取最少分割次数,一个简单的思路是穷尽所有分割情况,再从中找出分割后可构成回文子串且次数最少的分割方法。

解题思路:对于输入字符串如s=“aab”,一个直观的思路是判断“aab”是否构成回文,根据回文的特点是对称性,那么,我们可以判断s[0]与s[2]是否相等,不等,因此“aab”不能构成回文,此后再怎么判断呢???这种无章法的操作没有意义,因为一个字符串构成回文的情况是很复杂的,对于一个长度为n的字符串,其构成回文的子串长度可能的长度分布范围可以是1—n:整体构成回文如“baab”,则子串长度可为n=4;如“cab”,只能构成长度为1的回文子串。

鉴于上述分析,对于一个字符串,我们需要考虑所有可能的分割,这个问题可以抽象成一个DP问题。

这里需要两次dp:

(1)判断是否为回文字符串

对于一个长度为n的字符串,设 pd[i][j] 表示第i个字符到第j个字符是否构成回文,其状态转移方程为 p[i][j] = (s[i] == s[j]) && p[i+1][j-1],其中 p[i][j] = true if [i, j]为回文

(2)需要切割几次

一维的dp数组,其中dp[i]表示子串 [0, i] 范围内的最小分割数,那么我们最终要返回的就是 dp[n-1]。

我们需要判断区间 [j, i] 内的子串是否为回文串了,是回文串,dp[i] =1 + dp[j-1] 

class Solution {
public:
    int minCut(string s) {
        int n = s.size();
        if(n == 0) return 0;
        vector> pd(n,vector(n));
        vector res(n,0);
        // 注意这里i和j的迭代,保证计算[j][i]时,[j+1][i-1]已经存在!!!
        for(int i = 0;i < n;i++){
            // 最开始初始化为i,最多0-i切i次
            res[i] = i;
            for(int j = 0;j <= i;j++){
                if(s[i] == s[j] && (i-j <= 2 || pd[j+1][i-1])){
                    pd[j][i] = true;
                    // 不断往前探索j-i区间,如果j-i为回文串,则更新
                    res[i] = (j==0)? 0:min(res[i],res[j-1]+1);
                }
            }
        }
        return res[n-1];
    }
};

 

Maximal Rectangle(最大矩形)(hard)

Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and return its area.

Example:

Input:
[
  ["1","0","1","0","0"],
  ["1","0","1","1","1"],
  ["1","1","1","1","1"],
  ["1","0","0","1","0"]
]
Output: 6

思路:这道题的二维矩阵每一层向上都可以看做一个直方图,输入矩阵有多少行,就可以形成多少个直方图,对每个直方图都调用 Largest Rectangle in Histogram 直方图中最大的矩形 中的方法,就可以得到最大的矩形面积。

(1)那么这道题唯一要做的就是将每一层构成直方图,由于题目限定了输入矩阵的字符只有 '0' 和 '1' 两种,所以处理起来也相对简单。方法是,对于每一个点,如果是‘0’,则赋0,如果是 ‘1’,就赋 之前的height值加上1。

(2)对每一行调用计算最大直方图

class Solution {
public:
    int maximalRectangle(vector>& matrix) {
        if(matrix.empty() || matrix[0].empty()) return 0;
        
        int n = matrix.size(),m = matrix[0].size(),max_rec = 0;
        vector> height(n,vector(m));
        // 计算高度
        for(int i = 0;i < n;i++){
            for(int j = 0;j < m;j++){
                if(matrix[i][j] == '1'){
                    height[i][j] = i == 0? 1:height[i-1][j]+1;
                }
                else height[i][j] = 0;
            }
        }
        // 每一行都可以调用计算直方图的最大矩形
        for(int i = 0;i < n;i++){
            max_rec = max(max_rec,getMaxRectangle(height[i]));
        }
        return max_rec;
    }
    
    int getMaxRectangle(vector row){
        int n = row.size();
        int max_rec = 0,minH;
        for(int i = 0;i < n;i++){
            while(i < n-1 && row[i+1] >= row[i]) i++;
            minH = row[i];
            for(int j = i;j >= 0;j--){
                minH = min(row[j],minH);
                max_rec = max(max_rec,minH*(i-j+1));
            }
        }
        return max_rec;
    }
};

还有一种方法可参考:https://www.cnblogs.com/grandyang/p/4322667.html

 

Best Time to Buy and Sell Stock III(hard)

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete at most two transactions(最多执行两次交易).

Note: You may not engage in multiple transactions(必须先卖出再买入) at the same time (i.e., you must sell the stock before you buy again).

Example 1:

Input: [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
             Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.

Example 2:

Input: [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
             Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are
             engaging multiple transactions at the same time. You must sell before buying again.

Example 3:

Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

思路:

参考:https://blog.csdn.net/linhuanmars/article/details/23236995

这题很难,这里可以是k次交易

(1)global[i][j]:当前到达第 i 天,可以最多进行 j 次交易(不一定一定要交易到 j 次),最好的利润是多少

global[i][j] = max(local[i][j], global[i - 1][j])

当前局部最好的,和过往全局最好的中大的那个(因为最后一次交易如果包含当前天一定在局部最好的里面,否则一定在过往全局最优的里面)

(2)local[i][j]:当前到达第 i 天,最多可进行 j 次交易,并且最后一次交易在当天卖出的最好的利润是多少

它们的递推式为:

local[i][j] = max(global[i - 1][j - 1] + max(diff, 0), local[i - 1][j] + diff)

第一个是全局到i-1天进行j-1次交易,然后加上今天的交易,如果今天是赚钱的话(也就是前面只要j-1次交易,最后一次交易取当前天),如果今天不赚钱,就让其今天买进再卖出,这样也增加了一次交易,最后一次交易也在今天

第二个量则是取local第i-1天j次交易,然后加上今天的差值(这里因为local[i-1][j]比如包含第i-1天卖出的交易,所以现在变成第i天卖出,并不会增加交易次数,而且这里无论diff是不是大于0都一定要加上,因为否则就不满足local[i][j]必须在最后一天卖出的条件了)。

class Solution {
public:
    int maxProfit(vector& prices) {
        if(prices.empty()) return 0;
        int n = prices.size();
        // 注意这里是3,否则下面出现j-1的时候会有问题,这里让j=1的时候,j-1的值为0
        int local[n][3] = {0};
        int global[n][3] = {0};
        for(int i = 1;i < n;i++){
            int diff = prices[i]-prices[i-1];
            for(int j = 1;j <= 2;j++){
                local[i][j] = max(local[i-1][j]+diff,global[i-1][j-1]+max(0,diff));
                global[i][j] = max(global[i-1][j],local[i][j]);
            }
        }
        return global[n-1][2];
    }
};

 

Best Time to Buy and Sell Stock with Cooldown(hard)

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times) with the following restrictions:

  • You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).
  • After you sell your stock, you cannot buy stock on next day. (ie, cooldown 1 day)

Example:

Input: [1,2,3,0,2]
Output: 3 
Explanation: transactions = [buy, sell, cooldown, buy, sell]

思路:

参考:https://www.cnblogs.com/jdneo/p/5228004.html

这道题可以用动态规划的思路解决。但是一开始想的时候总是抽象不出状态转移方程来,之后看到了一种用状态机的思路,觉得很清晰,特此拿来分享,先看如下状态转移图

s0表示在第i天之前最后一个操作是冷冻期(此时既可以继续休息,又可以开始买入),此时的最大收益。

s1表示在第i天之前最后一个操作是买(此时可以卖出,可以休息),此时的最大收益。

s2表示在第i天之前最后一个操作是卖(此时只能休息),此时的最大收益。

leetcode(10) || 动态规划 && 位操作_第1张图片

这里我们把状态分成了三个,根据每个状态的指向,我们可以得出下面的状态转移方程:

  • s0[i] = max(s0[i-1], s2[i-1])
  • s1[i] = max(s1[i-1], s0[i-1] - price[i])
  • s2[i] = s1[i-1] + price[i]

这样就清晰了很多。

class Solution {
public:
    int maxProfit(vector& prices) {
        int n = prices.size();
        if(n < 2) return 0;
        vector s0(n);
        vector s1(n);
        vector s2(n);
        s0[0] = 0;
        s1[0] = -prices[0];
        // 注意这里不能定义为0
        s2[0] = INT_MIN;
        for(int i = 1;i < n;i++){
            s0[i] = max(s0[i-1],s2[i-1]);
            s1[i] = max(s0[i-1]-prices[i],s1[i-1]);
            s2[i] = s1[i-1]+prices[i];
        }
        return max(s0[n-1],s2[n-1]);
    }
};

 

Minimum Path Sum(mid)

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

Example:

Input:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
Output: 7
Explanation: Because the path 1→3→1→1→1 minimizes the sum.

思路:这题挺简单的,我们维护一个二维的dp数组,其中dp[i][j]表示当前位置的最小路径和

class Solution {
public:
    int minPathSum(vector>& grid) {
        if(grid.empty() || grid[0].empty()) return 0;
        int m = grid.size(),n = grid[0].size();
        vector> dp(m,vector(n));
        dp[0][0] = grid[0][0];
        for(int i = 1;i < m;i++) dp[i][0] = dp[i-1][0]+grid[i][0];
        for(int j = 1;j < n;j++) dp[0][j] = dp[0][j-1]+grid[0][j];
        for(int i = 1;i < m;i++){
            for(int j = 1;j < n;j++){
                dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }
};

 

Interleaving String(hard)

Given s1s2s3, find whether s3 is formed by the interleaving of s1 and s2.

Example 1:

Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
Output: true

Example 2:

Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
Output: false

思路:

只要是遇到字符串的子序列或是匹配问题直接就上动态规划Dynamic Programming,其他的都不要考虑,什么递归呀的都是浮云,千辛万苦的写了递归结果拿到OJ上妥妥Time Limit Exceeded,能把人气昏了,所以还是直接就考虑DP解法省事些。

可以用递归做,每匹配s1或者s2中任意一个就递归下去。但是会超时。

DP设计:

s1, s2只有两个字符串,因此可以展平为一个二维地图,判断是否能从左上角走到右下角

边界条件:

这道题的大前提是字符串s1和s2的长度和必须等于s3的长度,如果不等于,肯定返回false。那么当s1和s2是空串的时候,s3必然是空串,则返回true。所以直接给dp[0][0]赋值true,然后若s1和s2其中的一个为空串的话,那么另一个肯定和s3的长度相等,则按位比较,若相同且上一个位置为True,赋True,其余情况都赋False,这样的二维数组dp的边缘就初始化好了。

递推关系:

在任意非边缘位置dp[i][j]时,s1到达第i个元素,s2到达第j个元素:

它的左边或上边有可能为True或是False,如果左边和右上都是false,则此路不通

否则,判断该位置是否与s3位置匹配

地图上往右一步就是s2[j-1]匹配s3[i+j-1]。

地图上往下一步就是s1[i-1]匹配s3[i+j-1]。

示例:s1="aa",s2="ab",s3="aaba"。标1的为可行。最终返回右下角。

     0  a  b

0   1  1  0

a   1  1  1

a   1  0  1

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n1 = s1.size(),n2 = s2.size(),n3 = s3.size();
        // 长度判断
        if(n1+n2 != n3) return false;
        if(n1 == 0) return s2 == s3;
        if(n2 == 0) return s1 == s3;
        vector> m(n1+1,vector(n2+1));
        for(int i = 0;i <= n1;i++){
            for(int j = 0;j <= n2;j++){
                if(i == 0 && j == 0) m[i][j] = true;
                else if(i == 0) m[i][j] = m[i][j-1] && (s2[j-1] == s3[j-1]);
                else if(j == 0) m[i][j] = m[i-1][j] && (s1[i-1] == s3[i-1]);
                // 递推关系式
                else m[i][j] = (m[i-1][j] && s1[i-1] == s3[i+j-1]) || (m[i][j-1] && s2[j-1] == s3[i+j-1]);
            }
        }
        return m[n1][n2];
    }
};

 

Scramble String

Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursively(这里分割可以是任意处的分割,而不是二分).

Below is one possible representation of s1 = "great":

    great
   /    \
  gr    eat
 / \    /  \
g   r  e   at
           / \
          a   t

To scramble the string, we may choose any non-leaf node and swap its two children.

For example, if we choose the node "gr" and swap its two children, it produces a scrambled string "rgeat".

    rgeat
   /    \
  rg    eat
 / \    /  \
r   g  e   at
           / \
          a   t

We say that "rgeat" is a scrambled string of "great".

Similarly, if we continue to swap the children of nodes "eat" and "at", it produces a scrambled string "rgtae".

    rgtae
   /    \
  rg    tae
 / \    /  \
r   g  ta  e
       / \
      t   a

We say that "rgtae" is a scrambled string of "great".

Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1.

Example 1:

Input: s1 = "great", s2 = "rgeat"
Output: true

Example 2:

Input: s1 = "abcde", s2 = "caebd"
Output: false

题意:判断一个字符串是否为另一个字符串“乱序”得到,这种乱序采用的方式是将一个字符串从某个位置“割开”,形成两个子串,然后对两个子串进行同样的“割开”操作,直到到达叶子节点,无法再分割。然后对非叶子节点的左右孩子节点进行交换,最后重新从左至右组合形成新的字符串,由于这个过程中存在字符位置的变化,因此,原来的字符串顺序可能会被打乱,当然也可能没有(同一个非叶子节点的左右孩子交换0次或偶数次,就无变化)。


思路1:递归加上剪枝

传统递归时间复杂度太高,这里使用对子串排序,如果两者排序后不相同,直接剪枝,从而降低时间复杂度

class Solution {
public:
    bool isScramble(string s1, string s2) {
        int m = s1.size(),n = s2.size();
        if(m != n) return false;
        if(s1 == s2) return true;
        // 使用排序剪枝
        string str1 = s1,str2 = s2;
        sort(str1.begin(),str1.end());
        sort(str2.begin(),str2.end());
        if(str1 != str2) return false;
        // 递归
        for(int i = 1;i < m;i++){
            if(isScramble(s1.substr(0,i),s2.substr(0,i)) && isScramble(s1.substr(i,m-i),s2.substr(i,m-i)))
                return true;
            if(isScramble(s1.substr(0,i),s2.substr(m-i)) && isScramble(s1.substr(i),s2.substr(0,m-i)))
                return true;
        }
        return false;
    }
};

 

Decode Ways(mid)

A message containing letters from A-Z is being encoded to numbers using the following mapping:

'A' -> 1
'B' -> 2
...
'Z' -> 26

Given a non-empty string containing only digits, determine the total number of ways to decode it.

Example 1:

Input: "12"
Output: 2
Explanation: It could be decoded as "AB" (1 2) or "L" (12).

Example 2:

Input: "226"
Output: 3
Explanation: It could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

思路:这题不难,但是!!!对0的处理很麻烦,老是会漏掉情况,要考虑清楚100,101这类情况

class Solution {
public:
    int numDecodings(string s) {
        if(s.empty()) return 0;
        if(s[0] == '0') return 0;
        int n = s.size(),front = s[0]-'0';
        vector dp(n+1,1);
        for(int i = 2;i < n+1;i++){
            int num = s[i-1]-'0';
            // 当前数为0的情况!!!注意110,100
            if(num == 0){
                if(front == 0 || front*10 > 26) return 0;
                dp[i-1] = dp[i-2];
                dp[i] = dp[i-1];
            }
            // 当前数不为0,判断能否组成数字,这里要排除前一个为0的情况
            else if(front != 0 && front*10+num <= 26) dp[i] = dp[i-2]+dp[i-1];
            else dp[i] = dp[i-1];
            front = num;
        }
        return dp[n];
    }
};

 

Distinct Subsequences(hard)

Given a string S and a string T, count the number of distinct subsequences of S which equals T.

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).

Example 1:

Input: S = "rabbbit", T = "rabbit"
Output: 3
Explanation:

As shown below, there are 3 ways you can generate "rabbit" from S.
(The caret symbol ^ means the chosen letters)

rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

Example 2:

Input: S = "babgbag", T = "bag"
Output: 5
Explanation:

As shown below, there are 5 ways you can generate "bag" from S.
(The caret symbol ^ means the chosen letters)

babgbag
^^ ^
babgbag
^^    ^
babgbag
^    ^^
babgbag
  ^  ^^
babgbag
    ^^^

思路:这题我竟然做出来了,总结下主要两个套路:

(1)把要求变换的两个字符串写成横竖形式

(2)dp数组里记录的内容就是我们所要求的内容

leetcode(10) || 动态规划 && 位操作_第2张图片

class Solution {
public:
    long long numDistinct(string s, string t) {
        int m = s.size(),n = t.size();
        if(m <= n) return s==t;
        vector> dp(n+1,vector(m+1,0));
        for(int j = 0;j < m+1;j++) dp[0][j] = 1;
        for(int i = 1;i < n+1;i++){
            for(int j = i;j < m+1;j++){
                if(s[j-1] == t[i-1]) dp[i][j] = dp[i-1][j-1]+dp[i][j-1];
                else dp[i][j] = dp[i][j-1];
            }
        }
        return dp[n][m];
    }
};

 

Word Break(mid)

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

Note:

  • The same word in the dictionary may be reused multiple times in the segmentation.
  • You may assume the dictionary does not contain duplicate words.

Example 1:

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".

Example 2:

Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple".
             Note that you are allowed to reuse a dictionary word.

Example 3:

Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false

思路:如果直接暴力做,其实很难,因为每次字段都长短起始不一,所以我们用dp数组存放前面的匹配情况,保存中间的计算结果

我们就用一个一维的dp数组,其中dp[i]表示范围[0, i)内的子串是否可以拆分,注意这里dp数组的长度比s串的长度大1,是因为我们要handle空串的情况,我们初始化dp[0]为true,然后开始遍历。

注意这里我们需要两个for循环来遍历,因为此时已经没有递归函数了,所以我们必须要遍历所有的子串,我们用j把[0, i)范围内的子串分为了两部分,[0, j) 和 [j, i),其中范围 [0, j) 就是dp[j],范围 [j, i) 就是s.substr(j, i-j),其中dp[j]是之前的状态,我们已经算出来了,可以直接取,只需要在字典中查找s.substr(j, i-j)是否存在了,如果二者均为true,将dp[i]赋为true,并且break掉,此时就不需要再用j去分[0, i)范围了,因为[0, i)范围已经可以拆分了。

leetcode(10) || 动态规划 && 位操作_第3张图片

class Solution {
public:
    bool wordBreak(string s, vector& wordDict) {
        unordered_set word_set(wordDict.begin(),wordDict.end());
        int n = s.size();
        vector dp(n+1);
        dp[0] = true;
        for(int i = 0;i < n+1;i++){
            for(int j = 0;j < i;j++){
                if(dp[j] && word_set.count(s.substr(j,i-j))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

 

Word Break II(hard)

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences.

Note:

  • The same word in the dictionary may be reused multiple times in the segmentation.
  • You may assume the dictionary does not contain duplicate words.

Example 1:

Input:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
Output:
[
  "cats and dog",
  "cat sand dog"
]

Example 2:

Input:
s = "pineapplepenapple"
wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
Output:
[
  "pine apple pen apple",
  "pineapple pen apple",
  "pine applepen apple"
]
Explanation: Note that you are allowed to reuse a dictionary word.

Example 3:

Input:
s = "catsandog"
wordDict = ["cats", "dog", "sand", "and", "cat"]
Output:
[]

思路:

直接用递归会超时,因为举例来说:

如果当s变成 "sanddog"的时候,那么此时我们知道其可以拆分成sand和dog,当某个时候如果我们又遇到了这个 "sanddog"的时候,我们难道还需要再调用递归算一遍吗,当然不希望啦,所以我们要将这个中间结果保存起来,由于我们必须要同时保存s和其所有的拆分的字符串,那么可以使用一个HashMap,来建立二者之间的映射。

字典中保存{ "sanddog":["sand dog"]}

那么在递归函数中

(1) 我们首先检测当前s是否已经有映射,有的话直接返回即可

(2) 如果s为空了,我们如何处理呢,题目中说了给定的s不会为空,但是我们递归函数处理时s是会变空的,这时候我们是直接返回空集吗,这里有个小trick,我们其实放一个空字符串返回,为啥要这么做呢?我们观察题目中的Output,发现单词之间是有空格,而最后一个单词后面没有空格,所以这个空字符串就起到了标记当前单词是最后一个,那么我们就不要再加空格了。

class Solution {
public:
    vector wordBreak(string s, vector& wordDict) {
        if(s.empty() || wordDict.empty()) return {};
        unordered_map> m;
        return dfs(s,wordDict,m);
    }
    
    // 注意这里需要加&,否则需要新建变量,会超时!!!!!
    vector dfs(string s,vector& wordDict,unordered_map> &m){
        if(s.empty()) return {""};
        if(m.count(s)) return m[s];
        vector res;
        for(auto &word:wordDict){
            if(s.substr(0,word.size()) != word) continue;
            vector cur = dfs(s.substr(word.size()),wordDict,m);
            for(auto str:cur){
                res.push_back(word + (str.empty()? "":" ") + str);
            }
        }
        return m[s] = res;
    }
};

 

Dungeon Game

The demons had captured the princess (P) and imprisoned her in the bottom-right corner of a dungeon. The dungeon consists of M x N rooms laid out in a 2D grid. Our valiant knight (K) was initially positioned in the top-left room and must fight his way through the dungeon to rescue the princess.

The knight has an initial health point represented by a positive integer. If at any point his health point drops to 0 or below, he dies immediately.

Some of the rooms are guarded by demons, so the knight loses health (negative integers) upon entering these rooms; other rooms are either empty (0's) or contain magic orbs that increase the knight's health (positive integers).

In order to reach the princess as quickly as possible, the knight decides to move only rightward or downward in each step.

 

Write a function to determine the knight's minimum initial health so that he is able to rescue the princess.

For example, given the dungeon below, the initial health of the knight must be at least 7 if he follows the optimal path RIGHT-> RIGHT -> DOWN -> DOWN.

-2 (K) -3 3
-5 -10 1
10 30 -5 (P)

Note:

  • The knight's health has no upper bound.
  • Any room can contain threats or power-ups, even the first room the knight enters and the bottom-right room where the princess is imprisoned.

思路:

这题看着简单,其实有一个难点:如果从起始位置开始遍历,我们并不知道初始时应该初始化的血量,但是到达公主房间后,我们知道血量至少不能小于1,如果公主房间还需要掉血的话,那么掉血后剩1才能保证起始位置的血量最小。

故这道题应该逆向处理:

最先处理的是公主所在的房间的起始生命值,然后慢慢向第一个房间扩散,不断的得到各个位置的最优的生命值,逆向推正是本题的精髓所在。

dp中存储着该位置到公主所在房间所需的最低血量,如果该血量为负数,则设置成1即可

leetcode(10) || 动态规划 && 位操作_第4张图片

class Solution {
public:
    int calculateMinimumHP(vector>& dungeon) {
        if(dungeon.empty() || dungeon[0].empty()) return 0;
        auto m = dungeon.size(),n = dungeon[0].size();
        vector> dp(m+1,vector(n+1,INT_MAX));
        dp[m-1][n] = 1; dp[m][n-1] = 1;
        for(int i = m-1;i >= 0;i--){
            for(int j = n-1;j >= 0;j--){
                dp[i][j] = max(1,min(dp[i+1][j],dp[i][j+1])-dungeon[i][j]);
            }
        }
        return dp[0][0];
    }
};

优化:其实使用一个一维数组覆盖即可

leetcode(10) || 动态规划 && 位操作_第5张图片

class Solution {
public:
    int calculateMinimumHP(vector>& dungeon) {
        if(dungeon.empty() || dungeon[0].empty()) return 0;
        auto m = dungeon.size(),n = dungeon[0].size();
        vector dp(n+1,INT_MAX);
        // 注意这里是n-1为1
        dp[n-1] = 1;
        for(int i = m-1;i >= 0;i--){
            for(int j = n-1;j >= 0;j--){
                dp[j] = max(1,min(dp[j],dp[j+1])-dungeon[i][j]);
            }
        }
        return dp[0];
    }
};

 

House Robber(打家劫舍)(easy)

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
             Total amount you can rob = 1 + 3 = 4.

Example 2:

Input: [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
             Total amount you can rob = 2 + 9 + 1 = 12.

思路:最开始写个个直接间隔算的,但是这样是会有问题的,比如[2,1,1,2]这种会有问题

正确应该使用dp记录 到达该位置时最大数,由于不能抢相邻的,所以我们可以用再前面的一个的dp值加上当前的房间值,和当前房间的前面一个dp值比较,取较大值当做当前dp值,所以我们可以得到状态转移方程dp[i] = max(num[i] + dp[i - 2], dp[i - 1])

class Solution {
public:
    int rob(vector& nums) {
        if(nums.empty()) return 0;
        int n = nums.size(),max_num;
        if(n == 1) return nums[0];
        vector dp(n+2);
        for(int i = 0;i < n;i++){
            dp[i+2] = max(dp[i+1],dp[i]+nums[i]);
        }
        return dp[n+1];
    }
};

 

House Robber II(mid)

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2),
             because they are adjacent houses.

Example 2:

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
             Total amount you can rob = 1 + 3 = 4.

思路:相较上一题,这题变成了一个圈,算两次即可,第一次抢劫第一家,最后一家不抢,第二次不抢第一家,最后一家可以抢

class Solution {
public:
    int rob(vector& nums) {
        auto n = nums.size();
        if(n == 0) return 0;
        if(n == 1) return nums[0];
        vector dp(n);
        // 第一次抢劫第一家,最后一家不抢
        int first;
        dp[0] = nums[0]; dp[1] = nums[0];
        for(int i = 2;i < n - 1;i++){
            dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
        }
        first = dp[n-2];
        // 第二次不抢第一家,最后一家可以抢
        dp[0] = 0; dp[1] = nums[1];
        for(int i = 2;i < n;i++){
            dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
        }
        return max(first,dp[n-1]);
    }
};

 

House Robber III(hard)

The thief has found himself a new place for his thievery again. There is only one entrance to this area, called the "root." Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that "all houses in this place forms a binary tree". It will automatically contact the police if two directly-linked houses were broken into on the same night.

Determine the maximum amount of money the thief can rob tonight without alerting the police.

Example 1:

Input: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \ 
     3   1

Output: 7 
Explanation: Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.

Example 2:

Input: [3,4,5,1,3,null,1]

     3
    / \
   4   5
  / \   \ 
 1   3   1

Output: 9
Explanation: Maximum amount of money the thief can rob = 4 + 5 = 9.

思路:这题用上面的递归做是不行的,会重复计算,对于下面这种情况无法计算

     3
    / \
   14   5
  / \   \ 
 1   3   16

这种问题是很典型的递归问题,我们可以利用回溯法来做,因为当前的计算需要依赖之前的结果,

(1)对于某一个节点,如果其左子节点存在,我们通过递归调用函数,算出不包含左子节点返回的值,同理,如果右子节点存在,算出不包含右子节点返回的值

(2)那么此节点的最大值可能有两种情况,一种是该节点值加上不包含左子节点和右子节点的返回值之和,另一种是左右子节点返回值之和不包含当期节点值,取两者的较大值返回即可

(3)但是这种方法无法通过OJ,超时了,所以我们必须优化这种方法,这种方法重复计算了很多地方,比如要完成一个节点的计算,就得一直找左右子节点计算,我们可以把已经算过的节点用哈希表保存起来,以后递归调用的时候,现在哈希表里找,如果存在直接返回,如果不存在,等计算出来后,保存到哈希表中再返回,这样方便以后再调用

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int rob(TreeNode* root) {
        if(!root) return 0;
        unordered_map m;
        return dfs(root,m);
    }
    
    // 注意m前面的&,否则超时!!!
    int dfs(TreeNode* root,unordered_map &m){
        if(!root) return 0;
        if(m.count(root)) return m[root];
        int val = root->val;
        if(root->left) val += dfs(root->left->left,m)+dfs(root->left->right,m);
        if(root->right) val += dfs(root->right->left,m)+dfs(root->right->right,m);
        val = max(val,(dfs(root->left,m)+dfs(root->right,m)));
        // 加入字典不要忘!!!
        m[root] = val;
        return val;
    }
};

 

Range Sum Query - Immutable(easy)

Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.

Example:

Given nums = [-2, 0, 3, -5, 2, -1]

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

Note:

  1. You may assume that the array does not change.
  2. There are many calls to sumRange function.

思路:这题确实简单,使用一个dp数组,存放0-i直接的和即可

class NumArray {
public:
    NumArray(vector nums) {
        int sum = 0;
        for(int i = 0;i < nums.size();i++){
            sum += nums[i];
            dp.push_back(sum);
        }
    }
    
    int sumRange(int i, int j) {
        if(i == 0) return dp[j];
        else return dp[j]-dp[i-1];
    }

private:
    vector dp;
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(i,j);
 */

 

range Sum Query 2D - Immutable(medium)

Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2).

leetcode(10) || 动态规划 && 位操作_第6张图片
The above rectangle (with the red border) is defined by (row1, col1) = (2, 1) and (row2, col2) = (4, 3), which contains sum = 8.

Example:

Given matrix = [
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]

sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12

Note:

  1. You may assume that the matrix does not change.
  2. There are many calls to sumRegion function.
  3. You may assume that row1 ≤ row2 and col1 ≤ col2.

思路:情况比上面复杂一点,建立一个累计区域和的数组,然后根据边界值的加减法来快速求出给定区域之和。

这里我们维护一个二维数组dp,其中dp[i][j]表示累计区间(0, 0)到(i, j)这个矩形区间所有的数字之和,那么此时如果我们想要快速求出(r1, c1)到(r2, c2)的矩形区间时,只需dp[r2][c2] - dp[r2][c1 - 1] - dp[r1 - 1][c2] + dp[r1 - 1][c1 - 1]即可

下面的代码中我们由于用了辅助列和辅助行,所以下标会有些变化,增加一列全0,一行全0,方便计算

class NumMatrix {
public:
    NumMatrix(vector> matrix) {
        if(matrix.empty() || matrix[0].empty()) return;
        dp.resize(matrix.size()+1, vector(matrix[0].size()+1,0));
        for(int i = 1;i <= matrix.size();i++){
            for(int j = 1;j <= matrix[0].size();j++){
                dp[i][j] = dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+matrix[i-1][j-1];
            }
        }
    }
    
    int sumRegion(int row1, int col1, int row2, int col2) {
        return dp[row2+1][col2+1]+dp[row1][col1]-dp[row1][col2+1]-dp[row2+1][col1];
    }
    
private:
    vector > dp;
};

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */

 

Valid Permutations for DI Sequence(递增递减数列个数)(hard)

We are given S, a length n string of characters from the set {'D', 'I'}. (These letters stand for "decreasing" and "increasing".)

valid permutation is a permutation P[0], P[1], ..., P[n] of integers {0, 1, ..., n}, such that for all i:

  • If S[i] == 'D', then P[i] > P[i+1], and;
  • If S[i] == 'I', then P[i] < P[i+1].

How many valid permutations are there?  Since the answer may be large, return your answer modulo 10^9 + 7.

Example 1:

Input: "DID"
Output: 5
Explanation: 
The 5 valid permutations of (0, 1, 2, 3) are:
(1, 0, 3, 2)
(2, 0, 3, 1)
(2, 1, 3, 0)
(3, 0, 2, 1)
(3, 1, 2, 0)

Note:

  1. 1 <= S.length <= 200
  2. S consists only of characters from the set {'D', 'I'}.

思路:

这题直接dfs暴力是不行的,加上剪枝时间复杂度仍然超级高

思路应该是dp,难点是怎样dp

假如我们现在已经有了一个 "DID" 模式的序列 1032,假如我们还想加一个D,变成 "DIDD",该怎么加数字呢?

多了一个模式符,就多了一个数字4,显然直接加4是不行的,实际是可以在末尾加2的,但是要先把原序列中大于等于2的数字都先加1,即 1032 -> 1043,然后再加2,变成 10432,就是 "DIDD" 了。虽然我们改变了序列的数字顺序,但是升降模式还是保持不变的。同理,也是可以加1的,1032 -> 2043 -> 20431,也是可以加0的,1032 -> 2143 -> 21430。但是无法加3和4,因为 1032 最后一个数字2很很重要,所有小于等于2的数字,都可以加在后面,从而形成降序。那么反过来也是一样,若要加个升序,比如变成 "DIDI",猜也猜的出来,后面要加大于2的数字,然后把所有大于等于这个数字的地方都减1,比如加上3,1032 -> 1042 -> 10423,再比如加上4,1032 -> 1032 -> 10324。

通过上面的分析,我们知道了最后一个位置的数字的大小非常的重要,不管是要新加升序还是降序,最后的数字的大小直接决定了能形成多少个不同的序列,这个就是本题的隐藏信息,所以我们在定义 dp 数组的时候必须要把最后一个数字考虑进去,这样就需要一个二维的 dp 数组,其中 dp[i][j] 表示由范围 [0, i] 内的数字组成且最后一个数字为j的不同序列的个数

https://www.cnblogs.com/grandyang/p/11094525.html

class Solution:
    def numPermsDISequence(self, S: str) -> int:
        mod = 1e9+7
        n = len(S)
        res = 0
        dp = [[0]*(n+1) for _ in range(n+1)]
        dp[0][0] = 1
        for i in range(1,n+1):
            if S[i-1] == 'D':
                # 递减,新结尾最大是i-1,范围[0,i-1]
                for j in range(0,i):
                    # 上一个串结尾,至少是j,将前面的串的数大于等于j的都+1
                    for k in range(j,i):
                        dp[i][j] = (dp[i][j] + dp[i-1][k])%mod
            else:
                # 递增,新结尾至少是1,[1,i]
                for j in range(1,i+1):
                    # 上一个串结尾,至多是j-1,将前面的串的数大于等于j的都+1
                    for k in range(0,j):
                        dp[i][j] = (dp[i][j] + dp[i-1][k])%mod
        res = sum(dp[n])%mod
        return int(res)

 

 

位操作

Number of 1 Bits(easy)

Write a function that takes an unsigned integer and return the number of '1' bits it has (also known as the Hamming weight).

Example 1:

Input: 00000000000000000000000000001011
Output: 3
Explanation: The input binary string 00000000000000000000000000001011 has a total of three '1' bits.

思路:这题太简单了。。。

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int cnt = 0;
        for(int i = 0;i < 32;i++){
            if(n & 1 == 1) cnt++;
            n = n >> 1;
        }
        return cnt;
    }
};

 

Reverse Bits(easy)

Reverse bits of a given 32 bits unsigned integer.

Example 1:

Input: 00000010100101000001111010011100
Output: 00111001011110000010100101000000
Explanation: The input binary string 00000010100101000001111010011100 represents the unsigned integer 43261596, so return 964176192 which its binary representation is 00111001011110000010100101000000.

Example 2:

Input: 11111111111111111111111111111101
Output: 10111111111111111111111111111111
Explanation: The input binary string 11111111111111111111111111111101 represents the unsigned integer 4294967293, so return 3221225471 which its binary representation is 10101111110010110010011101101001.

Note:

  • Note that in some languages such as Java, there is no unsigned integer type. In this case, both input and output will be given as signed integer type and should not affect your implementation, as the internal binary representation of the integer is the same whether it is signed or unsigned.
  • In Java, the compiler represents the signed integers using 2's complement notation. Therefore, in Example 2 above the input represents the signed integer -3 and the output represents the signed integer -1073741825.

思路:注意这里是逆置,不是每个位取反,注意看题。故这题比较简单,把要翻转的数从右向左一位位的取出来(通过n = n & 1取出,n = n >> 1移动),如果取出来的是1,我们将结果res左移一位并且加上1;如果取出来的是0,我们将结果res左移一位,然后将n右移一位即可

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        uint32_t res = 0;
        for(int i = 0;i < 32;i++){
            if(n & 1 == 1) res = (res << 1) + 1;
            else res = res << 1;
            n = n >> 1;
        }
        return res;
    }
};

 

Repeated DNA Sequences(medium)

All DNA is composed of a series of nucleotides abbreviated as A, C, G, and T, for example: "ACGAATTCCG". When studying DNA, it is sometimes useful to identify repeated sequences within the DNA.

Write a function to find all the 10-letter-long sequences (substrings) that occur more than once in a DNA molecule.

Example:

Input: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"

Output: ["AAAAACCCCC", "CCCCCAAAAA"]

思路:找到所有出现2次及以上的字符串,使用自定义的map和hash方法会让算法的效率得到很大的提高

class Solution {
public:
    vector findRepeatedDnaSequences(string s) {
        unordered_set string_set;
        if (s.length() <= 10) {
            return {};
        }
        unordered_map m;
        for (int i = 0; i < s.length() - 9; ++i) {
            string tmp = s.substr(i, 10);
            if (m.find(tmp) == m.end()) {
                m[tmp] = 1;
            } else {
                string_set.insert(tmp);
            }
        }
        return vector(string_set.begin(), string_set.end());
    }
};

解法二:

使用位运算,速度更快,使用空间更少

构成输入字符串的字符只有四种,分别是A, C, G, T,下面我们来看下它们的ASCII码用二进制来表示:

A: 0100 0001  C: 0100 0011  G: 0100 0111  T: 0101 0100

由于我们的目的是利用位来区分字符,当然是越少位越好,通过观察发现,每个字符的后三位都不相同,故而我们可以用末尾三位来区分这四个字符。而题目要求是10个字符长度的串,每个字符用三位来区分,10个字符需要30位,在32位机上也OK。为了提取出后30位,我们还需要用个mask,取值为0x7ffffff,用此mask可取出后27位,再向左平移三位即可。

class Solution {
public:
    vector findRepeatedDnaSequences(string s) {
        vector res;
        if (s.length() <= 10) return res;
        // 0x为16进制标志,无意义
        int mask = 0x7ffffff,cur = 0;
        unordered_map m;
        
        // 前9位
        for (int i = 0; i < 9; ++i) {
            cur = (cur << 3) | (s[i] & 7);
        }
        for(int i = 9;i < s.size();++i){
            // (mask & cur) 取27位然后再左移3位
            // (s[i] & 7) 取最后3位
            cur = ((mask & cur) << 3) | (s[i] & 7);
            if(m.count(cur)){
                if(m[cur] == 1) res.push_back(s.substr(i-9,10));
                ++m[cur];
            } 
            else m[cur] = 1;
        }
        return res;
    }
};

 

Gray Code

The gray code is a binary numeral system where two successive values differ in only one bit.

Given a non-negative integer n representing the total number of bits in the code, print the sequence of gray code. A gray code sequence must begin with 0.

Example 1:

Input: 2
Output: [0,1,3,2]
Explanation:
00 - 0
01 - 1
11 - 3
10 - 2

For a given n, a gray code sequence may not be uniquely defined.
For example, [0,2,3,1] is also a valid gray code sequence.

00 - 0
10 - 2
11 - 3
01 - 1

Example 2:

Input: 0
Output: [0]
Explanation: We define the gray code sequence to begin with 0.
             A gray code sequence of n has size = 2n, which for n = 0 the size is 20 = 1.
             Therefore, for n = 0 the gray code sequence is [0].

思路:格雷码主要特点是两个相邻数的代码只有一位二进制数不同的编码,格雷码的处理主要是位操作 Bit Operation

然后我们来看看其他的解法,参考维基百科上关于格雷码的性质,有一条是说镜面排列的,n位元的格雷码可以从n-1位元的格雷码以上下镜射后加上新位元的方式快速的得到,如下图所示一般。

leetcode(10) || 动态规划 && 位操作_第7张图片

有了这条性质,我们很容易的写出代码如下:

class Solution {
public:
    vector grayCode(int n) {
        if(n == 0) return {0};
        vector res = {0,1};
        for(int i = 1;i < n;i++){
            int len = res.size();
            for(int j = len-1;j >= 0;j--){
                res.push_back(res[j] | (1 << i));
            }
        }
        return res;
    }
};

 

Single Number(easy)

Given a non-empty array of integers, every element appears twice except for one. Find that single one.

Note:

Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

Example 1:

Input: [2,2,1]
Output: 1

Example 2:

Input: [4,1,2,1,2]
Output: 4

这题很巧妙,利用计算机按位储存数字的特性来做的,因为每个数如果相同就出现两次,利用异或操作可以变为0,剩下那一个就是只出现一次的。用到的知识是A^A=0,两个相同的数异或变为0,然后0^B=B,这样即可找到出现一次的数。

class Solution {
public:
    int singleNumber(vector& nums) {
        int res = 0;
        for(auto &num:nums) res = res^num;
        return res;
    }
};

 

Single Number II(mid)

Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one.

Note:

Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

Example 1:

Input: [2,2,3,2]
Output: 3

Example 2:

Input: [0,1,0,1,0,1,99]
Output: 99

思路:我们可以建立一个32位的数字,来统计每一位上1出现的个数,我们知道如果某一位上为1的话,那么如果该整数出现了三次,对3去余为0,我们把每个数的对应位都加起来对3取余,最终剩下来的那个数就是单独的数字。

class Solution {
public:
    int singleNumber(vector& nums) {
        int res = 0;
        for(int i = 0;i < 32;i++){
            int sum = 0;
            for(int j = 0;j < nums.size();j++){
                sum += (nums[j] >> i) & 1;
            }
            // 对每一位统计所有nums上1的个数,去除以3,除不尽的即表示这一位有出现一次的数
            res |= (sum % 3) << i;
        }
        return res;
    }
};

 

Single Number III(mid)

Given an array of numbers nums, in which exactly two elements appear only once and all the other elements appear exactly twice. Find the two elements that appear only once.

Example:

Input:  [1,2,1,3,2,5]
Output: [3,5]

Note:

  1. The order of the result is not important. So in the above example, [5, 3] is also correct.
  2. Your algorithm should run in linear runtime complexity. Could you implement it using only constant space complexity?

思路:这题的难点是在最后得到ab异或的结果以后,怎样去将ab分开

(1)首先我们先把原数组全部异或起来,那么我们会得到一个数字,这个数字是两个不相同的数字异或的结果

(2)我们取出其中任意一位为‘1’的位,用来后面去区分a,b

就拿题目中的例子来说,如果我们将其全部亦或起来,我们知道相同的两个数亦或的话为0,那么两个1,两个2,都抵消了,就剩3和5亦或起来,那么就是二进制的11和101亦或,得到110。

然后我们先初始化diff为1,如果最右端就是1,那么diff与a_xor_b去做与会得到1,否则是0,我们左移diff,用10再去做与,此时得到结果11,找到最右端第一个1了

(3)用diff来和数组中每个数字相与

根据结果的不同,一定可以把3和5区分开来,而其他的数字由于是成对出现,所以区分开来也是成对的,最终都会亦或成0,不会3和5产生影响。分别将两个小组中的数字都异或起来,就可以得到最终结果了

class Solution {
public:
    vector singleNumber(vector& nums) {
        int a_xor_b = 0,diff = 1,a = 0,b = 0;
        // 得到ab异或结果
        for(auto &num:nums) a_xor_b ^= num;
        // 得到ab异或结果里从右往左第一个1,其实这里只要获得一个1在的位置都可以,因为这个位置表示ab在此位置不同
        while(!(a_xor_b & diff)) diff = diff << 1;
        // 通过这个位置是否为1,把整个数组分成2组,此时ab在两组内,每个组内是成对的数和1个a/b,从而分离
        for(auto &num:nums){
            if(num & diff) a ^= num;
            else b ^= num;
        }
        return {a,b};
    }
};

 

Power of Two

Given an integer, write a function to determine if it is a power of two.

Example 1:

Input: 1
Output: true 
Explanation: 20 = 1

Example 2:

Input: 16
Output: true
Explanation: 24 = 16

Example 3:

Input: 218
Output: false

思路:这道题要注意-2147483648

关于c++ int的表示范围:https://blog.csdn.net/y12345678904/article/details/52854230

我们来观察下2的次方数的二进制写法的特点:

1     2       4         8         16   ....

1    10    100    1000    10000 ....

那么我们很容易看出来2的次方数都只有一个1,剩下的都是0,所以我们的解题思路就有了,我们只要每次判断最低位是否为1,然后向右移位,最后统计1的个数即可判断是否是2的次方数。(一定要判断输入大于0)

class Solution {
public:
    bool isPowerOfTwo(int n) {
        int cnt = 0;
        while(n > 0){
            if(n & 1) cnt++;
            n = n >> 1;
        }
        if(cnt == 1) return true;
        else return false;
    }
};

解法二:

那么它的二进数必然是最高位为1,其它都为0,那么如果此时我们减1的话,则最高位会降一位,其余为0的位现在都为变为1,那么我们把两数相与,就会得到0

class Solution {
public:
    bool isPowerOfTwo(int n) {
        return (n > 0) && (!(n & (n-1)));
    }
};

 

Missing Number(easy)

Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, find the one that is missing from the array.

Example 1:

Input: [3,0,1]
Output: 2

Example 2:

Input: [9,6,4,2,3,5,7,0,1]
Output: 8

Note:
Your algorithm should run in linear runtime complexity. Could you implement it using only constant extra space complexity?

思路:

既然0到n之间少了一个数,我们将这个少了一个数的数组合0到n之间完整的数组异或一下,那么相同的数字都变为0了,剩下的就是少了的那个数字了(异或或者直接加减都可以)

class Solution {
public:
    int missingNumber(vector& nums) {
        int res = 0,i = 1;
        for(auto &num:nums){
            res ^= i++;
            res ^= num;
        }
        return res;
    }
};

 

Maximum Product of Word Lengths(mid)

Given a string array words, find the maximum value of length(word[i]) * length(word[j]) where the two words do not share common letters. You may assume that each word will contain only lower case letters. If no such two words exist, return 0.

Example 1:

Input: ["abcw","baz","foo","bar","xtfn","abcdef"]
Output: 16 
Explanation: The two words can be "abcw", "xtfn".

Example 2:

Input: ["a","ab","abc","d","cd","bcd","abcd"]
Output: 4 
Explanation: The two words can be "ab", "cd".

Example 3:

Input: ["a","aa","aaa","aaaa"]
Output: 0 
Explanation: No such pair of words.

思路:题目中说都是小写字母,那么只有26位,一个整型数int有32位,我们可以用后26位来对应26个字母,若为1,说明该对应位置的字母出现过,那么每个单词的都可由一个int数字表示两个单词没有共同字母的条件是这两个int数相与为0

class Solution {
public:
    int maxProduct(vector& words) {
        vector word_masks;
        int max_pro = 0;
        for(int i = 0;i < words.size();i++){
            int mask = 0;
            for(auto &c:words[i]) mask |= (1 << (c-'a'));
            for(int j = 0;j < i;j++){
                if(!(mask & word_masks[j])) max_pro = max(max_pro,int(words[i].size()*words[j].size()));
            }
            word_masks.push_back(mask);
        }
        return max_pro;
    }
};

 

Bitwise AND of Numbers Range

Given a range [m, n] where 0 <= m <= n <= 2147483647, return the bitwise AND of all numbers in this range, inclusive.

Example 1:

Input: [5,7]
Output: 4

Example 2:

Input: [0,1]
Output: 0

思路:m到n之间数相与的结果,其实就是m和n左边最长的公共1的部分,如:

[5, 7]里共有三个数字,分别写出它们的二进制为:

101  110  111

相与后的结果为100,仔细观察我们可以得出,最后的数是该数字范围内所有的数的左边共同的部分。

如果上面那个例子不太明显,我们再来看一个范围[26, 30],它们的二进制如下:

11010  11011  11100  11101  11110

class Solution {
public:
    int rangeBitwiseAnd(int m, int n) {
        int i = 0;
        while(m != n){
            m = m >> 1;
            n = n >> 1;
            i++;
        }
        return m << i;
    }
};

 

Power of Three(easy)

Given an integer, write a function to determine if it is a power of three.

Example 1:

Input: 27
Output: true

Example 2:

Input: 0
Output: false

Example 3:

Input: 9
Output: true

Example 4:

Input: 45
Output: false

Follow up:
Could you do it without using any loop / recursion?

思路:这题不能位操作哎,用循环的话就不断去除3

class Solution {
public:
    bool isPowerOfThree(int n) {
        while(n && n % 3==0) n = n/3;
        return n == 1;
    }
};

 

解法二:不用递归来做

利用对数的换底公式来做,高中学过的换底公式为logab = logcb / logca,那么如果n是3的倍数,则log3n一定是整数,我们利用换底公式可以写为log3n = log10n / log103,注意这里一定要用10为底数,不能用自然数或者2为底数,否则当n=243时会出错,原因请看这个帖子。现在问题就变成了判断log10n / log103是否为整数,在c++中判断数字a是否为整数,我们可以用 a - int(a) == 0 来判断

class Solution {
public:
    bool isPowerOfThree(int n) {
        // 判断int(log10(n) / log10(3)) - log10(n) / log10(3)是否为0,即可判断n是否为3的power
        return (n > 0 && int(log10(n) / log10(3)) - log10(n) / log10(3) == 0);
    }
};

 

补充一下基础位操作:

判断一个二进制数中是否存在两个连续的1

bool is_true(int n){
    return (n & n >> 1) > 0;
};

快速统计二进制中1的个数

def my_function(int n){
    int count = 0;
	while (num)
	{
		num = num&(num - 1);
		count++;
	}
    return count;
}

 

你可能感兴趣的:(LeetCode刷题记录)