第185场力扣(LeetCode)周赛 “生成数组” 问题

抽时间做了一下第185场的力扣(LeetCode)周赛,最后一题“生成数组”问题,把自己绕进去了,绕了很久才想通,在这里给大家分享一下。

题目描述

首先题目位于生成数组这里,简要描述如下:
给定了这么一个算法:


在求最大值的时候计算最大值的更新次数 search_cost。
题目要求给定3个正整数n, m, k,生成一个长度为n的数组arr,其中数组每个元素取值都满足 1 ≤ a r r [ i ] ≤ m , i ∈ [ 0 , n − 1 ] 1 \leq arr[i] \leq m, i \in [0,n-1] 1arr[i]m,i[0,n1],而把这个数组带入上述算法,search_cost值恰好为k,问给定n, m, k三个数能生成的数组有多少个,答案对 ( 1 0 9 + 7 ) (10^9+7) (109+7)取余。
一个例子如下:
输入:n = 2, m = 3, k = 1
输出:6
解释:可能的数组分别为 [1, 1], [2, 1], [2, 2], [3, 1], [3, 2] [3, 3]
(更多示例参考LeetCode本题描述,详见上文链接)
注意最大值初始值是-1而要求的数组全是正整数所以cost至少为1。

分析

首先,本题最暴力的解法很容易想到,那就是全排列,既然我想知道有多少个长度为n,取值范围1-m的数组满足要求(通过上述算法计算的cost为k),那我找到所有可能的数组逐一判断就可以了。显然,可能的数组有 m n m^n mn个,极其庞大的计算量。那么在次基础上优化一下呢?比如我们知道,如果前面排了一串数字,当前剩余 i 个位置没有排,并且还需要的cost大于 i,那意味着即使剩余的数全都大于之前找到的最大值并且呈递增趋势,依然无法满足cost的要求,就可以不再找下去了;再有,当已经出现的最大数为j,那么剩余能够引起cost增长的数必须大于j,且小于等于m,如果还需要的cost大于(m-j)那同样不可能满足要求,等等。
这样的思路是有剪枝的暴力搜索,复杂度仍较高,而且哪些情况需要继续搜下去,哪些需要减掉也让人很头大。那么我们自然就想到了“按照特定顺序的有记忆搜索”也就是DP大法了。

这道题一说动规,肯定一下想到的是三维的动态规划,那么构造三维动态规划表 dp[n+1][m+1][k+1]
那问题来了,表中的一项dp[i][j][h]代表什么呢?
实话实说,我确实是先想到构造这个表用DP方法,然后才开始思考状态转移和每一项的含义的。
首先我觉得i和h的参数应该没有争议,就是长度为i的数组cost为h时候的生成数组数量,那么j指的是什么?
我一开始认为,j表示数据可取的范围是[1,j],但这里涉及到一个问题,那就是并没有规定在i-1之前放置的数的最大最小值,也就是说,这里取到的数到底最大是多少,这关系到后续多大的数才能引起cost增加的问题,这个条件太松了。
那么我们能不能定义j表示第i个数一定以j结尾呢?这样一来,类似的,我们不知道j是不是之前出现过最大的数,以及出现过最大的数有多大。
因此,这里的j表示在数组[0,i]范围呢出现的最大数为j的情况,换言之,
dp[i][j][h]表示长度为i,出现最大数为j(元素范围[1,j]并且严格以j为最大值)且cost为h的数组数量。
那么状态转移方程如何来写?首先我们知道,我希望在i处放一个数,使其cost为h,那就一定有两种情况:

  1. 放置的数没有更新最大值,这个数小于等于已经出现的最大值,根本不会让上述算法进入if条件内;
  2. 放置的数是新出现的最大值,它的出现使这个数组的cost增加了1.

对于情况1,因为我们要求的是最大值为j且cost为h的数组,加入第i个数cost不更新,就意味着在前i-1的数组中最大值已经是j了,因为如果i-1的最大值大于j,那么他不能用于更新最大值等于j时的情况;如果i-1的最大值小于j,那么要使加入第i个数后最大值变为j,一定要加入j而此时cost必然增大。所以我们实际上考虑的就是 d p [ i − 1 ] [ j ] [ h ] dp[i-1][j][h] dp[i1][j][h]这种情况,显然,此时在位置i加入任意一个 ≤ j \leq j j的数都满足要求,那么情况1就包含了
j ∗ d p [ i − 1 ] [ j ] [ h ] j*dp[i-1][j][h] jdp[i1][j][h]
的数量。
其含义是,对于任意一个长度为i-1, 最大数为j,且cost为h的数组,只要我在位置i加入一个小于等于j的数,得到的结果都是长度为 i,最大数为 j,cost为h的数组。

对于情况2,显然如果数组长度为i-1的时候出现的最大数小于j,那当前放置了j才能让数组长度i时最大值为j,而这一过程必然增加了cost(出现了新的最大值),也就是:
∑ 1 ≤ j j < j d p [ i − 1 ] [ j j ] [ h − 1 ] \sum_{1 \leq jj <j}dp[i-1][jj][h-1] 1jj<jdp[i1][jj][h1]
情况2包含的数组数量,是所有长度为 i-1,最大值小于 j,且cost为 h-1 的数组数量之和,在这种情况下只要在位置i放上数字j,就可以更新最大值为j且cost为h了。

d p [ i ] [ j ] [ h ] dp[i][j][h] dp[i][j][h]只有通过上述两种情况才能得到。
找到了状态转移方程,就可以写出如下的三维DP的算法(C++版):

int numOfArrays(int n, int m, int k) {
  if (k > m || k > n) return 0;
  uint64_t mod = 1000000007;
  vector<vector<vector<uint64_t>>> dp(n + 1, vector<vector<uint64_t>>(m + 1, vector<uint64_t>(k + 1, 0)));
  dp[1][1][1] = 1;
  for (int j = 1; j <= m; ++j) dp[1][j][1] = 1;
  for (int i = 2; i <= n; ++i) {
    for (int j = 1; j <= m; ++j) {
      for (int h = 1; h <= k; ++h) {
        if (h > i || h > j) dp[i][j][h] = 0;
        else {
          dp[i][j][h] = dp[i - 1][j][h] * j % mod;
          for (int jj = 1; jj < j; ++jj) {
            dp[i][j][h] += dp[i - 1][jj][h - 1] % mod;
          }
        }
      }
    }
  }
  uint64_t res = 0;
  for (int j = 1; j <= m; ++j) {
    res += dp[n][j][k];
    res %= mod;
  }
  return res;
}

这里要注意,这个dp数组的初始化和起始位置。
显然当i=1, h=1时,也就是数组长度为1,cost取值也为1时,就等于放置一个单独的数,那么j的取值(最大值)是多少我们就放多少,且只有这一种情况,所以所有的 d p [ 1 ] [ j ] [ 1 ] dp[1][j][1] dp[1][j][1]都是1;

另外我是从i=2,j=1,h=1开始更新的,因为i=1时,除了上述h=1的情况外,剩余的都是h>i的情况,就是要在长度不足cost的数组内实现最大值更新cost次,这即使是完全递增都做不到,所以这些位置取值一定是0.

dp逐一计算时,也进行了判断,当h>i或h>j时数量都是0,因为没有足够的长度或者足够的数字能满足这个cost;除此之外, d p [ i ] [ j ] [ h ] dp[i][j][h] dp[i][j][h]的更新就是按照我们前面讲的两种情况,相加就是结果,注意要及时取余避免溢出。

在输出答案时,并不是 d p [ n ] [ m ] [ k ] dp[n][m][k] dp[n][m][k]就是最后答案,因为按照dp表的定义,这个值表示长度为n最大值严格等于m且cost为k的数量,但要达到长度n且cost为k,并不一定就要让最大值达到m,很可能只用一部分数就实现了,因此,最后的答案要考虑全部的j取值,也就是
∑ 1 ≤ j ≤ m d p [ n ] [ j ] [ k ] \sum_{1 \leq j \leq m}dp[n][j][k] 1jmdp[n][j][k]
同样每次加完取个余。

进一步优化

显然,通过上面的分析和代码,我们注意到 d p [ i ] [ j ] [ h ] dp[i][j][h] dp[i][j][h]只跟 d p [ i − 1 ] dp[i-1] dp[i1]的情况有关,因此完全可以优化成2个2维的表,一个记录i-1的数据,一个更新i的数据,交替进行即可。
C++代码如下:

int numOfArrays(int n, int m, int k) {
  if (k > m || k > n) return 0;
  uint64_t mod = 1000000007;
  vector<vector<uint64_t>> last(m + 1, vector<uint64_t>(k + 1, 0));
  vector<vector<uint64_t>> cur(m + 1, vector<uint64_t>(k + 1, 0));
  for (int j = 1; j <= m; ++j) last[j][1] = 1;
  for (int i = 2; i <= n; ++i) {
    for (int j = 1; j <= m; ++j) {
      for (int h = 1; h <= k; ++h) {
        if (h > i || h > j) cur[j][h] = 0;
        else {
          cur[j][h] = last[j][h] * j % mod;
          for (int jj = 1; jj < j; ++jj) {
            cur[j][h] += last[jj][h - 1] % mod;
          }
        }
      }
    }
    last = cur;
  }
  uint64_t res = 0;
  for (int j = 1; j <= m; ++j) {
    res += cur[j][k];
    res %= mod;
  }
  return res;
}

定义last和cur两个数组,首先初始化last,也就是i=1,h=1的情况,然后递增i用last去更新cur,思路不变,每个i更新完成后将cur赋值给last即可。

在这里插入图片描述
可以看到时间空间都得到了优化。当然这是LeetCode系统的运行结果(虽然我没想通为什么时间能快这么多,也许是二维表查找变快了吧。。。总之空间是一定会有优化的)

以上就是这道比较绕的题目的一些个人思考了,个人觉得这个题训练DP还是很有意思的,感兴趣的朋友可以前往LeetCode看一看,有更好的思路也欢迎分享。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值