2020年2月

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/super-egg-drop

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。

每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。

你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。

每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。

你的目标是确切地知道 F 的值是多少。无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

示例 1:

  • 输入:K = 1, N = 2
  • 输出:2
  • 解释:鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。如果它没碎,那么我们肯定知道 F = 2 。因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。

示例 2:

  • 输入:K = 2, N = 6
  • 输出:3

示例 3:

  • 输入:K = 3, N = 14
  • 输出:4

提示:

  • 1 <= K <= 100
  • 1 <= N <= 10000

二、动态规划+递归

看到题目的第一眼就能想到得用动态规划来解题了,关键是如何来定义动态规划的状态和状态转移方程。

比较容易想到的是以dp(k, n)来表示k个鸡蛋在n层建筑时需要扔鸡蛋的最小次数,它的最优状态肯定是从前面的1-n层楼中取,谁的状态越优,就取谁的。也就是说,在那一层扔鸡蛋需要的次数最少,就是谁。那么:

$$dp(k,n)=min(dp(k,1), dp(k,2), ..., dp(k,n))$$

在第i层扔鸡蛋,有两种结果:

  1. 鸡蛋碎了,这个时候鸡蛋少了一个,楼层也少一个,此时它的最优解是dp(k-1,i-1) + 1。
  2. 鸡蛋没碎,这个时候鸡蛋没少,楼层少了一个,最优解是dp(k,n-i) + 1。

可以参考这张图片:

图片来源:labuladong

后面的dp[k][n]和dp(k, n)是一个意思。

为什么没碎之后是dp[k][n-i],因为没碎的话,鸡蛋还是k个,而楼层只有n-i了,所以只要找出dp[k][n-i]就可以了。

计算出来这两种情况后,要选择最坏情况下的场景,所以要取两者的最大值。总结出来状态转移方程就是:

$$\begin{equation} dp[k][i]= \begin{cases} 1 & k=1 \\ 0 & n=0\\ max(dp[k-1][i-1], dp[k][n-i]) & k>1,n>0 \end{cases}\end{equation}$$

归纳成C++代码:

int dp(int k, int n, vector<vector<int>> &v) {
    int i, f = INT_MAX, dpi;
    
    if (k == 1) return n;
    if (n == 0) return 0;

    // v数组默认全是-1
    if (v[k][n] != -1) {
        return v[k][n];
    }

    // 遍历所有楼层
    for (i = 1; i <= n; i++) {
        // 计算出第i层的最优解
        dpi = max(dp(k - 1, i - 1, v), dp(k, n - i, v)) + 1;
        // 计算所有楼层的最小值
        f = min(f, dpi);
    }

    v[k][n] = f;
    return f;
}

时间复杂度

计算所有的鸡蛋+楼层组合需要时间复杂度O(KN),每个楼层取最优值是O(N),总体的时间复杂度是:

$$O(K*N^2)$$

这个时间复杂度还是很大的,可以说迄今为止我还没见过这么高复杂度的动态规划。提交后果然超时了:

在本地测试这组数据发现需要执行2s+才能计算出来(CPU是I5-8400主频2.8GHz),所以提交肯定会超时:

三、动态规划+二分

超时之后不得不想办法优化,优化前为了确定在哪个楼层扔鸡蛋最优不得不从1遍历到n,这个时间复杂度是O(N)太长了,实际上可以使用二分法来找出最优解,讲O(N)变成O(logN)。

上面已经求出了状态转移方程是max(dp[k-1][i-1], dp[k][n-i]),其中可以知道的是,dp[k-1][i-1]是随着i逐步递增的,因为楼层越高,需要扔的次数肯定也越多。而dp[k][n-i]是跟随i递减的,因子i是负数,i越大,n-i越小,扔的次数也越小。

上面的状态转移方程就是现在所有楼层的最大值中,找到最小值,如图所示:

红色的线表示的是两个子状态中的最大值,两根线的交点正好就是所有最大值中的最小值。那么根据这个规律,就可以把i∈[1, n]的遍历改成二分法来遍历,找到这个最小值。代码如下:

int dp(int k, int n, vector<vector<int>> &v) {
    int i, f = INT_MAX;
    int low, high, mid, broken, not_broken;
    
    if (k == 1) return n;
    if (n == 0) return 0;

    if (v[k][n] != -1) {
        return v[k][n];
    }

    low = 1;
    high = n;
    while (low <= high) {
        mid = (low + high) / 2;
        // 鸡蛋碎了
        broken = dp(k - 1, mid - 1, v);
        // 鸡蛋没碎
        not_broken = dp(k, n - mid, v);
        if (broken > not_broken) {
            high = mid - 1;
            f = min(f, broken + 1);
        } else {
            low = mid + 1;
            f = min(f, not_broken + 1);
        }
    }
    
    v[k][n] = f;
    return f;
}

修改后再测试上面超时的数据,只要4毫秒就可以执行完了,效率提高了几百倍:

再次提交代码,通过了(耗时较高):

把遍历变成二分后,遍历的O(N)变成了O(logN),所以总的时间复杂度是:O(KN*logN)​,空间复杂度是O(KN)。

四、终极动态规划

dp[k][m]表示k个鸡蛋扔m次最多可以确定的多少层的楼,那么第一个大于等于n的下标m就是扔鸡蛋的次数了。到第i层的时候,依旧是有两种策略:

  1. 鸡蛋碎了,此时鸡蛋减1,扔的次数减1,接下来可以检测出来的楼层是dp[k-1][m-1]
  2. 鸡蛋没碎,扔的次数减1,接下来可以检测出来的楼层是dp[k][m-1]

所以第i个鸡蛋能确定的楼层的总数是:dp[k-1][m-1] + dp[k][m-1] + 1,即鸡蛋碎了的时候能确定的楼层数加上鸡蛋没碎的时候能确定的楼层数再加上本层。

代码:

// k是鸡蛋个数,n是楼层,两个都大于等于1
int superEggDrop(int k, int n) {
    int i, j;
    vector<vector<int>> v(k + 1, vector<int>(n + 1, 0));

    for (j = 1; v[k][j - 1] < n; j++) { // 扔鸡蛋的次数
        for (i = 1; i <= k && v[k][j - 1] < n; i++) { // 鸡蛋个数
            v[i][j] = v[i - 1][j - 1] + v[i][j - 1] + 1;
        }
    }
    return j - 1;
}
要注意的地方是循环条件终止条件是v[k][j-1] >= n,为什么是j-1呢,因为第一层for循环的j表示的是第j次扔鸡蛋能确定的层数,此时层数还没有确定,还要在下面的循环中计算得到。如果使用v[k][j]会出现死循环导致数据溢出。

时间复杂度O(K*N),提交后执行用时大大减少:

继续优化

上面已经计算出来状态转移方程是:

$$dp[k][i]=dp[k-1][i-1] + dp[k][i-1] + 1$$

可以看到,当前状态实际上只与前面一列的状态有关,可以只保存前面一列状态的数据,空间复杂度为O(K)。

int superEggDrop(int k, int n) {
    int i, j;
    vector<vector<int>> v(k + 1, vector<int>(2, 0));
    // 第0列作为前一列的备份列,第1列作为当前列
    for (j = 1; v[k][0] < n; j++) {
        for (i = 1; i <= k && v[k][j - 1] < n; i++) {
            v[i][1] = v[i - 1][0] + v[i][0] + 1;
        }

        // 拷贝第1列到第0列
        for (i = 0; i <= k; i++) {
            v[i][0] = v[i][1];
        }
    }
    return j - 1;
}

[leetcode]300-最长上升子序列

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/longest-increasing-subsequence

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

  • 输入: [10,9,2,5,3,7,101,18]
  • 输出: 4
  • 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为O(n^2)。
进阶:你能将算法的时间复杂度降低到O(nlogn)吗?

二、题解

首先要搞清楚的是题意:求最长上升子序列。子序列不是子串,两者的区别是子序列可以不连续,子串必须连续

例如:[1, 3, 5, 2, 9],最长上升子序列是[1, 3, 5, 9],最长上升子串是[1, 3, 5]。

2.1 暴力法

题目的一个要求是用O(n^2)的时间复杂度来完成这个问题,如果有O(n^2)的时间复杂度,可以直接使用暴力破解的办法:

// 伪代码
for (i = 0; i < size; i++) {
    for (j = i; j < size; j++) {
        // 存在一个上升序列,和加一
        if (nums[j] >= nums[i])
            val[i][j] +=1;
    }
}

算法的时间复杂度和空间复杂度都是O(n^2)。

2.2 动态规划

dp[i]表示到第i个元素时的最大上升子序列长度,那么dp[i]就等于数组中上一个小于5的元素的最大上升子序列长度加1。

以[10,9,2,5,3,7]为例,i=5时如何求dp[i]?此时nums[i]=7,要想构造上升序列,它前面的数字都小于7才行。左边小于7的数字有三个:2、5和3。

  • 在数字1的地方,最大上升子序列的长度是1。
  • 在数字5的地方,最大上升子序列的长度是2;
  • 在数字3的地方,最大上升子序列的长度是2。

因为7大于三个数字,所以到达7的时候最大子序列长度肯定要加1,这个时候只有在上面三个数字中最大的上面加才能保证数字7的子序列最大。因此可以归纳出来的状态转移方程为:

$$dp[i]=max(dp[0], dp[1],..., dp[n]), n<i, nums[n]<=nums[i].$$

使用动态规划的时间复杂度也是O(n^2),空间复杂度O(n)。

2.3 二分法求解

二分法不容易想出来,即使是看题解也是看了几遍才明白。

考虑一个问题:假设存在数组tails,tails[i]表示最大上升子序列长度为i的子序列的末尾元素的最小值,这种情况下tails的长度就是最大子序列的长度了。存不存在这么一个数组呢,有没有办法构造出这样的一个数组?答案是肯定的。

以[10, 9, 2, 5, 7]为例解释tails数组每个元素的意义:

  1. 所有的数字都是长度为1的子序列,其中最小的数字是2,所以tails[1]=2。
  2. 长度为2的子序列有[2, 5], [2, 7]和[5, 7],其中末尾数最小的是5,所以tails[2]=5。
  3. 长度为3的子序列只有[2, 5, 7],末尾数只有一个就是7,所以tails[3]=7。

如果我们能构造出这个数组,那么问题就解决了。那么关键的问题是:如何知道遍历到哪个元素的时候,它的子序列长度是多少?

首先,暂且假设这个数组存在。结合tails数组的特性,可以确定的一个问题是tails[i]肯定大于tails[i-1],tails数组一定严格递增。因为题目求的是上升子序列,而tails数组保存的是每个子序列中的最小值,所以子序列右边的元素肯定大于左边的。

一个要注意的问题是tails[i]表示的是长度为i的子序列的末尾元素的最小值,以[1, 5, 3, 4]为例,最大上升子序列长度为2的有两个:[1, 5]和[1, 3],如果不取最小的末尾元素3,那么tails数组就是[1, 5],当遍历到4的时候,找到第一个大于它的数是5,此时下标等于2,也就是说最大上升子序列长度就是2。而实际上应该是3(子序列[1, 3, 4]),结果不对。

结合这个结论,在判断nums[i]的时候,只要从左到右找到第一个大于它的值的时候(使用二分法查找),这个下标就是它的最大子序列长度了。

这种情况下时间复杂度为O(nlogn)​,空间复杂度为O(n)。

三、代码

3.1 动态规划

int lengthOfLIS_DP(vector<int> &nums) {
    int res = 1;
    size_t i, j;
    vector<int> dp(nums.size(), 1);

    if (nums.empty())
        return 0;

    for (i = 0; i < nums.size(); i++) {
        // 遍历所有比当前数字小的元素
        for (j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                // 更新dp数组
                dp[i] = max(dp[i], dp[j] + 1);
                // 更新结果
                res = max(dp[i], res);
            }
        }
    }

    return res;
}

3.2 二分法

通过二分法找到区间内第一个大于目标的元素:

/*
 * 在区间[left, right]找到第一个大于等于目标的元素
 * @v 待查找的数组
 * @left 左边界,包含
 * @right 右边界,包含
 * @return 找到返回元素的下标,否则返回-1
 */
int find_first_ge(vector<int> &v, int left, int right, int target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (v[mid] >= target) {
            if (mid == left || v[mid - 1] < target)
                return mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return -1;
}

求最长上升子序列:

int lengthOfLIS_BINSEARCH(vector<int> &nums) {
    vector<int> tails(nums.size(), 0);
    int i, idx, max_len;

    if (nums.empty())
        return 0;

    max_len = 0;
    for (i = 0; i < nums.size(); i++) {
        idx = find_first_ge(tails, 0, max_len - 1, nums[i]);
        if (idx < 0) {
            // 没有找到一个比当前元素小的,扩充
            tails[max_len++] = nums[i];
        } else {
            // 找到了,替换掉
            tails[idx] = nums[i];
        }
    }
    return max_len;
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/house-robber

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

  • 输入: [1,2,3,1]
  • 输出: 4
  • 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

  • 输入: [2,7,9,3,1]
  • 输出: 12
  • 解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。

二、题解

假设当前是第n家房屋,已经偷到了x元。当前有两种选择:偷或者不偷。

  1. 如果偷了,那么说明前面一家房屋是不能偷的,否则会报警。这个时候偷到的金额为x[n - 2] + nums[n]。
  2. 如果不偷,说明前面一家房屋肯定是要偷的,不然少偷了一家。这个时候偷到的金额为x[n - 1]。

两种情况,如何选择?肯定是选择两者中金额更大的,那么就能得到状态转移方程为:

$$dp[i]=max(dp[i - 1], dp[i - 2] + nums[i])$$

因为当前状态只于前面两个状态有关,因此没必要分配dp数组,使用固定的变量来表示就行了(和斐波那契数列一样)。状态转移方程可修改为:

$$amount = max(prev, pprev + nums[n])$$

其中prev是上一个状态的最大值,pprev是上上个状态的最大值,nums[n]是当前第n个房屋的金额。

三、代码

int rob(vector<int> &nums) {
    int max_amount = 0, pprev, prev;
    size_t i;

    if (nums.size() < 1)
        return 0;

    pprev = 0;
    prev = nums[0];
    max_amount = prev;
    for (i = 1; i < nums.size(); i++) {
        max_amount = max(prev, pprev + nums[i]);
        pprev = prev, prev = max_amount;
    }
    return max_amount;
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

示例 1:

  • 输入:[7,1,5,3,6,4]
  • 输出:5
  • 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2

  • 输入:[7,6,4,3,1]
  • 输出:0
  • 解释:这种情况下, 没有交易完成, 所以最大利润为 0。

二、题解

利润最大化的条件:最高的卖出价格 - 最低的买入价格。保存一个截止目前的最低价格,并用当前价格减去这个最低价格,同时保存一份最高的利润,如果当前利润高于最高的利润就更新它。

三、代码

int maxProfit(vector<int> &prices) {
    int min_price = INT_MAX, max_profit = 0;
    for (auto i: prices) {
        // 更新最低价格:当前的最低价格低于之前的最低价格
        if (i < min_price) {
            min_price = i;
        }
        // 更新最大利润:当前的最大利润高于之前的最大利润
        if (i - min_price > max_profit) {
            max_profit = i - min_price;
        }
    }
    return max_profit;
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/coin-change

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

说明:

你可以认为每种硬币的数量是无限的。

二、题解

以示例一种的[1, 2, 5]三种硬币为例,当要凑到11块钱的时候,什么时候硬币个数最小?

只有在凑到11之前的硬币最少,凑够11的硬币才最少。凑到11块一种有三种可能性:

  1. 已经有10块了,再凑1块得到11块。
  2. 已经有9块了,再凑2块得到11块。
  3. 已经有6块了,再凑5块得到11块。

也就是说,手里面当前已经揣了10块、9块或是6块钱,这个时候哪个的硬币数量最少,凑成的11块钱硬币才最少。很明显又是一个动态规划,以dp[i]表示amount=i时候的最优解个数,那么dp[i]的状态转移方程为:

$$\begin{equation} dp(i)= \begin{cases} -1 & amount < 0\\ 0 & amount = 0 \\ min(dp[i-coin1], dp[i-coin2], ...) + 1 & amount > 0\end{cases} \end{equation}$$

三、代码

class Solution {
public:
    int coinChange(vector<int> &coins, int amount) {
        int i, cnt;
        // 初始化为int类型的最大值以方便做特殊处理
        vector<int> dp(amount + 1, INT_MAX);

        if (amount < 0)
            return -1;

        dp[0] = 0;
        for (i = 1; i <= amount; i++) {
            for (auto coin: coins) {
                // dp[i - coin]存在并且有最小值才行
                if (i - coin < 0 || dp[i - coin] == INT_MAX)
                    continue;
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }
        return dp[amount] == INT_MAX ? -1 : dp[amount];
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/wildcard-matching

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

  • '?' 可以匹配任何单个字符。
  • '*' 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

示例 1:

  • 输入:s = "aa", p = "a"
  • 输出:false
  • 解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

  • 输入:s = "aa", p = "*"
  • 输出: true
  • 解释: '*' 可以匹配任意字符串。

示例 3:

  • 输入:s = "cb", p = "?a"
  • 输出:false
  • 解释:'?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。

二、题解

动态规划,使用dp[i][j]表示字符串s的第i个字符模式串p的第j个字符的匹配状况,如果匹配上了设置为true,否则为false。

dp[i][j]何时为true:

  • s[i] == p[j]或者p[j] == '?'的时候,只要两个字符串前面的字符匹配上(也就是dp[i - 1][j - 1] = ture),那么这里的两个字符也匹配上。
  • s[i] == '*'的时候,有两种情况能匹配上:

    1. 字符串s的前面一个字符就已经能匹配上模式串p了,即dp[i - 1][j] = true时,如s=ab, p=ab*
    2. 模式串p的前面一个字符已经匹配上字符串s了,即dp[i][j - 1] = true时,如s=abcd, p=ab*
    3. 注意这里的状态并不是d[i - 1][j - 1]控制的,因为一个*可以匹配多个字符,而?只能匹配一个字符。

初始情况下:

  • dp[0][0] = true:空字符串s和空字符p,匹配上。
  • dp[0][j]:s为空,p不为空,只有p是*时值才为true。
  • dp[i][0]:s不为空,p为空,永远不可能匹配上,都是false。

基于以上几点,就可以写出代码了。

三、代码

bool isMatch(string s, string p) {
    int i, j;
    vector<vector<bool>> dp(s.length() + 1, vector<bool>(p.length() + 1, false));

    // 初始化数组
    dp[0][0] = true;
    for (j = 1; j <= p.length(); j++) {
        if (p[j - 1] == '*')
            dp[0][j] = dp[0][j - 1];
        else
            break;
    }

    for (i = 1; i <= s.length(); i++) {
        for (j = 1; j <= p.length(); j++) {
            if ((s[i - 1] == p[j - 1] || p[j - 1] == '?'))
                dp[i][j] = dp[i - 1][j - 1];
            if (p[j - 1] == '*' && (dp[i - 1][j] || dp[i][j - 1])) {
                dp[i][j] = true;
            }
        }
    }
    return dp[s.length()][p.length()];
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/matrix-cells-in-distance-order

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给出 R 行 C 列的矩阵,其中的单元格的整数坐标为 (r, c),满足 0 <= r < R 且 0 <= c < C。另外,我们在该矩阵中给出了一个坐标为 (r0, c0) 的单元格。返回矩阵中的所有单元格的坐标,并按到 (r0, c0) 的距离从最小到最大的顺序排。

其中,两单元格(r1, c1) 和 (r2, c2) 之间的距离是曼哈顿距离:|r1 - r2| + |c1 - c2|,你可以按任何满足此条件的顺序返回答案。

示例 1:

  • 输入:R = 1, C = 2, r0 = 0, c0 = 0
  • 输出:[[0,0],[0,1]]
  • 解释:从 (r0, c0) 到其他单元格的距离为:[0,1]

示例 2:

  • 输入:R = 2, C = 2, r0 = 0, c0 = 1
  • 输出:[[0,1],[0,0],[1,1],[1,0]]
  • 解释:从 (r0, c0) 到其他单元格的距离为:[0,1,1,2],[[0,1],[1,1],[0,0],[1,0]] 也会被视作正确答案。

示例 3:

  • 输入:R = 2, C = 3, r0 = 1, c0 = 2
  • 输出:[[1,2],[0,2],[1,1],[0,1],[1,0],[0,0]]
  • 解释:从 (r0, c0) 到其他单元格的距离为:[0,1,1,2,2,3]其他满足题目要求的答案也会被视为正确,例如 [[1,2],[1,1],[0,2],[1,0],[0,1],[0,0]]。

二、题目解析

思路:列出所有的节点并计算和单元格的距离,然后根据距离排序,最后返回所有的节点。

三、代码

不使用map,直接使用vector完成:

题解区看到的一个思路清晰、容易理解的代码。
class Solution {
private:
    static bool sortFunc(vector<int> &a, vector<int> &b) {
        return a[2] < b[2];
    }
public:
    vector<vector<int>> allCellsDistOrder(int R, int C, int r0, int c0) {
        int x, y, i = 0;
        vector<vector<int>> v(R*C, vector<int>(3));
        // 计算所有点的坐标和距离
        for (x = 0; x < R; x++) {
            for (y = 0; y < C; y++) {
                v[i][0] = x;
                v[i][1] = y;
                v[i][2] = abs(r0 - x) + abs(c0 - y); // 计算曼哈顿距离
                i++;
            }
        }
        // 根据曼哈顿距离排序
        sort(v.begin(), v.end(), sortFunc);
        // 弹出曼哈顿距离
        for (i = 0; i < R * C; i++) {
            v[i].pop_back();
        }
        return v;
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/split-array-largest-sum

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:

数组长度 n 满足以下条件:1 ≤ n ≤ 1000, 1 ≤ m ≤ min(50, n)

示例:

  • 输入:nums = [7,2,5,10,8], m = 2
  • 输出:18
  • 解释:一共有四种方法将nums分割为2个子数组,其中最好的方式是将其分为[7,2,5] 和 [10,8],因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

二、题目解析

咋一看题目,完全不知道如何下手。唯一能想到的一个办法是动态规划,但是动态规划要怎么规划。。。算了,看题解吧。

看了题解才知道要用贪心+二分,所谓贪心,就是一种策略思想,在满足条件的情况下寻找最优解。找最优解的办法就是二分,而实际上也就是不断通过二分遍历和去试。

假设数组为nums,要分成m个子数组。那么不难看出和肯定是在[max(nums), sum(nums)]的范围的:

  • 当m等于数组的长度n时,每个元素一个子数组,最大和是数组中的最大元素max(nums)
  • 当m等于1时,整个数组就是一个子数组,最大和是所有数组的和sum(nums)

在了解了这个特性后,就可以使用二分不断遍历范围内的和,然后判断和是不是满足数组条件的和。

判断和是否满足条件的办法:当前遍历到的和是sum,从数组开头开始遍历,计算累加和小于等于sum的子数组总量。如果子数组的数量大于m,说明子数组太多了,也就是sum太小了,把遍历和的左边界设置为sum+1。如果子数组的数量小于或者等于m,说明子数组太少了,sum设置的太大,修改右边界为sum-1。

以测试数据为例,[7,2,5,10,8]的和范围在[10, 32]之间,获取最小的最大和的办法:

第一步,取中值mid=21,计算出来符合条件的子数组为:

[7,2,5,10] [8]

子数组m的个数刚好等于2,设置right=mid-1=20,再计算mid=15,符合条件的数组:

[7,2,5] [10] [8]

子数组个数为3,大于要求的2,设置left=mid+1=16,计算mid=(16+20)/2=18,符合条件的数组:

[7,2,5] [10,8]

子数组个数为2,满足题目要求,设置right=mid-1=17,计算mid=(16+17)/2=16,符合条件的数组:

[7,2,5] [10] [8]

子数组个数为3,大于要求的2,设置left=mid+1=17,计算mid=(17+17)/2=17,符合条件的数组:

[7,2,5] [10] [8]

子数组个数还是3,大于要求的2,设置left=mid+1=18,此时left大于right(17),遍历结束,返回18。

三、示例代码

3.1 代码

class Solution {
public:
    int splitArray(vector<int> &nums, int m) {
        size_t i;
        int count = 0;
        long left = nums[0], right = 0, mid, sum;
        // 确定左边界
        for (i = 0; i < nums.size(); i++) {
            right += nums[i];
            left = left < nums[i] ? nums[i] : left;
        }

        while (left <= right) {
            mid = (left + right) / 2;
            sum = 0;
            count = 1; // count要初始化为1
            // 遍历所有的和
            for (i = 0; i < nums.size(); i++) {
                // 和超出了,重新计算下一个子数组
                if (sum + nums[i] > mid) {
                    sum = nums[i];
                    count ++;
                } else {
                    sum += nums[i];
                }
            }
            if (count > m) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return (int)left;
    }
};

3.2 为什么count初始化为1

在评论区有看到一个问题是问为什么count要初始化为1,回复是说默认情况下是整个数组,所以要加1:

对于这个答案,我是不认可的。

count加1的真正原因是因为最后一个子数组没有计算到count里面去,因为最后一个子数组走不到count++循环就退出了。

cnt=0   cnt=1    循环退出
 ↓          ↓         ↓
[]    [7,2,5]   [10, 8]

当循环退出的时候count来不及加1就退出了,正确的做法就是在循环结束后再自加一次,初始化为1的目的就是相当于提前加了这个1。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/next-greater-element-iii

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个32位正整数 n,你需要找到最小的32位整数,其与 n 中存在的位数完全相同,并且其值大于n。如果不存在这样的32位整数,则返回-1。

示例 1:

  • 输入: 12
  • 输出: 21

示例 2:

  • 输入: 21
  • 输出: -1

二、题目解析

题目意思梳理:给定一个数字n(假设是523486,六位数),用同样的数字组成(5、2、3、4、8、6)组合成另外一个六位数,要求这个新组成的数字是所有组合中第一个大于原数字的。注意n是32位的正整数,如果不存在的话返回-1。

看完题目可能并没有好的思路,可以先考虑一下,什么场景下会不存在这个数字。例如:

  • 99999
  • 98765
  • 33322

可以看出的一个规律是:如果数字都是降序排列的话,是找不到一个比它大数字的。为什么呢?如果一个数,低位的数字比高位的数字都小,那么不可能存在一种办法可以把低位较小的数字放到高位去反而比原来的数字还要大的。

只存在一种情况就是把低位较大的数字和高位较小的数字对换才会比原来数字大。那也就说明,如若要找到一个比原来数大的组合,这n的内部一定存在一个数,比它右边的数字要小。进而就说明,从低位往高位,一定存在降序数列。例如:

  • 95876 ==> 96578
  • 82431 ==> 83124

在知晓了这个情况之后,如何取得最小值呢?

以95876为例,从低位往高位数,第一个降序的位置是[..., 5, 8, ...],如果在这里用一个低位的大于5的数字替换掉它就可以得到一个大于95876的数字了。那么如何替换才能最小?肯定是把右边大于5的最小的数字替换过来才最小,也就是用6来替换了,替换后得到96875。

96875是最小的吗?不是,96578才是最小。96875和96578有什么区别?最后三位数字排序不一样,一个升序排列,一个降序排列。降序排列数字由大到小,排列出来肯定是大于升序序列的。

所以能总结出来的操作是:

  1. 从低位往高位寻找到一个降序序列[i, i+1],记录下i的位置。如果不存在这个序列,说明不存在符合条件的数字。
  2. 如果找到了i,从i的右边开始,找到一个最小的大于i的数字,替换它。
  3. 最后把i右边的数据反转,得到一个升序序列。

除此之外,还有一个要注意的问题是要考虑数据溢出,可能原始数据没有溢出,但是比它大的那个数字就溢出了。例如1999999999,比它大的最小的数字是9199999999,这个数字溢出了。

三、代码实现

3.1 考虑数字溢出

leetcode第七题“正数反转”就已经遇到过这个问题了。假设抓换后的当前数字为x,新计算出来的x = x * 10 + y。当x > INT_MAX / 10 或者x == INT_MAX / 10 && y > 7的时候,计算x会溢出,具体的可以参考:leetcode7-整数反转

3.2 数字反转

需要实现一个函数,反转固定区间内的数字:

/*
 * 反转数组,左闭右开区间[left, right)。
 * @left 左下标,包含当前值
 * @right 右下标,不包含当前值
 */
void reverse(vector<int> &v, size_t left, size_t right) {
    int tmp;
    size_t idx;
    for (idx = left; idx < (left + right) / 2; idx++) {
        tmp = v[idx];
        v[idx] = v[right - 1 - (idx - left)];
        v[right - 1 - (idx - left)] = tmp;
    }
}

3.3 交换函数

void my_swap(int &a, int &b) {
    int c = a;
    a = b;
    b = c;
}

3.4 实现

    int nextGreaterElement(int n) {
        int x = n, output = 0;
        int i, dec_idx = -1, y;
        vector<int> v;

        // 按位放到数组中去,并按照逻辑顺序把数组反转
        while (x) {
            v.push_back(x % 10);
            x /= 10;
        }
        reverse(v, 0, v.size());

        // 找到第一个下降的元素下标
        for (i = v.size() - 1; i > 0; i--) {
            if (v[i] > v[i - 1]) {
                dec_idx = i - 1;
                break;
            }
        }
        // 没有找到
        if (i == 0)
            return -1;

        // y是最小的大于v[dec_idx]的元素的下标,它不可能是-1,一定能找到
        y = dec_idx + 1;
        for (i = dec_idx + 2; i < v.size(); i++) {
            if (v[i] > v[dec_idx] && v[i] <= v[y]) {
                y = i;
            }
        }

        // 交换
        my_swap(v[dec_idx], v[y]);
        // 反转右边的数字
        reverse(v, dec_idx + 1, v.size());

        // 把数组转换成单个数字
        for (i = 0; i < v.size(); i++) {
            // 处理数据溢出
            if (output > INT_MAX / 10 || (output == INT_MAX && v[i] > (INT_MAX % 10)))
                return -1;
            output = output * 10 + v[i];
        }

        return output;
    }

在题解中看到的一个可以优化的点是:找到i后,从i开始往右边找到一个最小的大于它的数字,可以使用二分查找来完成。不过题目限制的是32位整数,总共不超过10位,个人认为优化不明显。

实际测试中,使用这个二分法查找,执行耗时更短。

四、单元测试

测试案例的后面几个都是leetcode提交中出错的案例:

TEST(nextGreaterElement, leetcode) {
    Solution s;
    EXPECT_EQ(s.nextGreaterElement(877645132), 877645213);
    EXPECT_EQ(s.nextGreaterElement(12), 21);
    EXPECT_EQ(s.nextGreaterElement(21), -1);
    EXPECT_EQ(s.nextGreaterElement(12222333), 12223233);
    EXPECT_EQ(s.nextGreaterElement(12443322), 13222344);
    EXPECT_EQ(s.nextGreaterElement(1999999999), -1);
}

一、二分查找

template<typename T>
int bin_search(const T data[], int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] > target) {
            right = mid - 1;
        } else if (data[mid] < target) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

二、查找最后一个小于n的值

// 查找第一个大于目标的元素
template<typename T>
int bin_search_first_greater(const T data[], int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] > target) {
            if (mid == left || data[mid - 1] < target)
                return mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return -1;
}

三、查找第一个大于n的值

template<typename T>
int bin_search_last_less(const T *data, int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] < target) {
            if (mid == right || data[mid + 1] > target)
                return mid;
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

四、查找第一个等于n的值

template<typename T>
int bin_search_first_equal(const T *data, int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] < target) {
            left = mid + 1;
        } else if (data[mid] > target) {
            right = mid - 1;
        } else {
            if (mid == left || data[mid - 1] < target)
                return mid;
            right = mid - 1;
        }
    }
    return -1;
}

五、查找最后一个等于n的值

template<typename T>
int bin_search_last_equal(const T *data, int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] < target) {
            left = mid + 1;
        } else if (data[mid] > target) {
            right = mid - 1;
        } else {
            if (mid == right || data[mid + 1] > target)
                return mid;
            left = mid + 1;
        }
    }
    return -1;
}

六、单元测试

6.1 TestFixtures

class bin_search_test : public ::testing::Test {
protected:
    void SetUp() override {
        //                              0    1   2  3  4  5  6  7   8    9
        arr1 = new int[test_ele_count]{-23, -5, -2, 3, 8, 8, 9, 11, 54, 100};
        arr2 = new int[test_ele_count]{-1, 1, 3, 5, 5, 5, 6, 6, 6, 6};
    }

    void TearDown() override {
        delete arr1;
        delete arr2;
    }

    int *arr1;
    int *arr2;
};

6.2 测试案例

// 测试案例:二分搜索
TEST_F(bin_search_test, bin_search) {
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 9), 6);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 54), 8);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, -23), 0);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 100), 9);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, -1), -1);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, -3), -1);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 7), -1);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 60), -1);
}

// 测试案例:第一个大于目标的值
TEST_F(bin_search_test, bin_search_first_greater) {
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, 0), 3);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, -5), 2);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, 100), -1);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, -24), 0);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, 15), 8);
}

// 测试案例:最后一个小于目标
TEST_F(bin_search_test, bin_search_last_less) {
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, 20), 7);
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, 90), 8);
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, -1), 2);
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, -40), -1);
}

// 测试案例:第一个等于目标
TEST_F(bin_search_test, bin_search_first_equal) {
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 5), 3);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 6), 6);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 1), 1);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 12), -1);
}

// 测试案例:最后一个等于目标
TEST_F(bin_search_test, bin_search_last_equal) {
    EXPECT_EQ(bin_search_last_equal(arr2, 0, test_ele_count - 1, 5), 5);
    EXPECT_EQ(bin_search_last_equal(arr2, 0, test_ele_count - 1, 6), 9);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 1), 1);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 12), -1);
}