41 四边形不等式技巧(上)

189 阅读5分钟

内容:

区间划分问题中的划分点不回退现象

四边形不等式技巧特征

  1. 两个可变参数的区间划分问题

    一个大的范围上的求解划分为两个小范围的求解

  2. 每个格子有枚举行为

  3. 当两个可变参数固定一个,另一个参数和答案之间存在单调性关系

    例如题目3中,3-17的合并代价一定比4-17的合并代价大

  4. 而且两组单调关系是反向的:(升 升,降 降) (升 降,降 升)

  5. 能否获得指导枚举优化的位置对:上+右,或者,左+下

    image-20220410122625809

四边形不等式技巧注意点

1,不要证明!用对数器验证! 2,枚举的时候面对最优答案相等的时候怎么处理?用对数器都试试! 3,可以把时间复杂度降低一阶 O(N^3) -> O(N^2) O(N^2 * M) -> O(N * M) O(N * M^2) -> O(N * M) 4,四边形不等式有些时候是最优解,有些时候不是 不是的原因:尝试思路,在根儿上不够好

题目:

题目1:

给定一个非负数组arr,长度为N, 那么有N-1种方案可以把arr切成左右两部分 每一种方案都有,min{左部分累加和,右部分累加和} 求这么多方案中,min{左部分累加和,右部分累加和}的最大值是多少? 整个过程要求时间复杂度O(N)

题目2:

把题目一中提到的,min{左部分累加和,右部分累加和},定义为S(N-1),也就是说: S(N-1):在arr[0…N-1]范围上,做最优划分所得到的min{左部分累加和,右部分累加和}的最大值 现在要求返回一个长度为N的s数组, s[i] =在arr[0…i]范围上,做最优划分所得到的min{左部分累加和,右部分累加和}的最大值 得到整个s数组的过程,做到时间复杂度O(N)

  • 当从0-i位置上的最优划分位置为s,那么从0-i+1范围上,最优划分只用从s位置往后试即可,因为非负(情况1:左部分是答案即左右部分中的最小值,那么s不应该往左移动。情况2:右部分为左右两部分的最小值。2.1 加上18位置的数字,左部分变成小的了,s不需要往左移动。2.2 加上18位置还是右部分小,还是不需要向左移动因为非负,并且如果需要往左移动时,那么当i=17的时候s就应该在s-1的位置所以s也不需要动。2.3 加上18位置后,左部分小,那么还是不需要动)所以,加上18,要么还是s要么向右移动,所以复杂度O(N)。

  • image-20220410114913866

    存在不回退,敏感点,指标和区间之间存在单调性

题目3:

摆放着n堆石子。现要将石子有次序地合并成一堆,规定每次只能选相邻的2堆石子合并成新的一堆 并将新的一堆石子数记为该次合并的得分,求出将n堆石子合并成一堆的最小得分(或最大得分)合并方案

  • image-20220410115703469

  • 范围动态规划模型O(N^3)

    L>R:❎

    对角线:由于就是1-1,2-2,3-3位置合并,代价就是0

    01,12,23:即两个数字合并代价

    02,13:根据前一个位置的代价可以计算

    image-20220410120448344

    从下往上,从左往右,因为需要左边的值,需要下方的值

  • 假如在3-16范围内最优划分在8,9右下方在4-17范围假设在12,13之间,能否猜测3-17范围内,最优划分在8,13之间?

    image-20220410121204999

    记录个best[ L ] [R]:记录L-R上最优划分的位置,i=j的时候都是0,从L=R-1的进行计算,划分点即L

    image-20220410121432346

题目4:

给定一个整型数组 arr,数组中的每个值都为正数,表示完成一幅画作需要的时间,再给定一个整数num 表示画匠的数量,每个画匠只能画连在一起的画作 所有的画家并行工作,返回完成所有的画作需要的最少时间 arr=[3,1,4],num=2。 最好的分配方式为第一个画匠画3和1,所需时间为4 第二个画匠画4,所需时间为4 所以返回4 arr=[1,1,1,4,3],num=3 最好的分配方式为第一个画匠画前三个1,所需时间为3 第二个画匠画4,所需时间为4 第三个画匠画3,所需时间为3 返回4 暴力解法

 public static int splitArray1(int[] nums, int K) {
    int N = nums.length;
    int[] sum = new int[N + 1];
    for (int i = 0; i < N; i++) {
       sum[i + 1] = sum[i] + nums[i];
    }
    int[][] dp = new int[N][K + 1];
    for (int j = 1; j <= K; j++) {
       dp[0][j] = nums[0];
    }
    for (int i = 1; i < N; i++) {
       dp[i][1] = sum(sum, 0, i);
    }
    // 每一行从上往下
    // 每一列从左往右
    // 根本不去凑优化位置对儿!
    for (int i = 1; i < N; i++) {
       for (int j = 2; j <= K; j++) {
          int ans = Integer.MAX_VALUE;
          // 枚举是完全不优化的!
          for (int leftEnd = 0; leftEnd <= i; leftEnd++) {
             int leftCost = leftEnd == -1 ? 0 : dp[leftEnd][j - 1];
             int rightCost = leftEnd == i ? 0 : sum(sum, leftEnd + 1, i);
             int cur = Math.max(leftCost, rightCost);
             if (cur < ans) {
                ans = cur;
             }
          }
          dp[i][j] = ans;
       }
    }
    return dp[N - 1][K];
 }
优化解法
  • image-20220410124528900

    根据最后一个画家负责哪个范围来进行划分

    image-20220410124749842

    image-20220410125024293

    符合特征 1 两个可变参数,画作数量,画家数量 2 每个格子有枚举行为 3 有单调性,画作增加,时长增加,画家增加,时间减少 4 单调性反向,画作增加,时长增加,画家增加,时间减少 5 存在加速对当前位置(i,j)左方(i,j-1)下方(i+1,j)共同算出左侧提供下限,下方提供上限,无下方值就无上限进行尝试

    image-20220410130834417

// 课上现场写的方法,用了枚举优化,O(N * K)
public static int splitArray2(int[] nums, int K) {
   int N = nums.length;
   int[] sum = new int[N + 1];
   for (int i = 0; i < N; i++) {
      sum[i + 1] = sum[i] + nums[i];
   }
   int[][] dp = new int[N][K + 1];
   int[][] best = new int[N][K + 1];
   for (int j = 1; j <= K; j++) {
      dp[0][j] = nums[0];
      best[0][j] = -1;
   }
   for (int i = 1; i < N; i++) {
      dp[i][1] = sum(sum, 0, i);
      best[i][1] = -1;
   }
   // 从第2列开始,从左往右
   // 每一列,从下往上
   // 为什么这样的顺序?因为要去凑(左,下)优化位置对儿!
   for (int j = 2; j <= K; j++) {
      for (int i = N - 1; i >= 1; i--) {
         int down = best[i][j - 1];
         // 如果i==N-1,则不优化上限
         int up = i == N - 1 ? N - 1 : best[i + 1][j];
         int ans = Integer.MAX_VALUE;
         int bestChoose = -1;
         for (int leftEnd = down; leftEnd <= up; leftEnd++) {
            int leftCost = leftEnd == -1 ? 0 : dp[leftEnd][j - 1];
            int rightCost = leftEnd == i ? 0 : sum(sum, leftEnd + 1, i);
            int cur = Math.max(leftCost, rightCost);
            // 注意下面的if一定是 < 课上的错误就是此处!当时写的 <= !
            // 也就是说,只有取得明显的好处才移动!
            // 举个例子来说明,比如[2,6,4,4],3个画匠时候,如下两种方案都是最优:
            // (2,6) (4) 两个画匠负责 | (4) 最后一个画匠负责
            // (2,6) (4,4)两个画匠负责 | 最后一个画匠什么也不负责
            // 第一种方案划分为,[0~2] [3~3]
            // 第二种方案划分为,[0~3] [无]
            // 两种方案的答案都是8,但是划分点位置一定不要移动!
            // 只有明显取得好处时(<),划分点位置才移动!
            // 也就是说后面的方案如果==前面的最优,不要移动!只有优于前面的最优,才移动
            // 比如上面的两个方案,如果你移动到了方案二,你会得到:
            // [2,6,4,4] 三个画匠时,最优为[0~3](前两个画家) [无](最后一个画家),
            // 最优划分点为3位置(best[3][3])
            // 那么当4个画匠时,也就是求解dp[3][4]时
            // 因为best[3][3] = 3,这个值提供了dp[3][4]的下限
            // 而事实上dp[3][4]的最优划分为:
            // [0~2](三个画家处理) [3~3] (一个画家处理),此时最优解为6
            // 所以,你就得不到dp[3][4]的最优解了,因为划分点已经越过2了
            // 提供了对数器验证,你可以改成<=,对数器和leetcode都过不了
            // 这里是<,对数器和leetcode都能通过
            // 这里面会让同学们感到困惑的点:
            // 为啥==的时候,不移动,只有<的时候,才移动呢?例子懂了,但是道理何在?
            // 哈哈哈哈哈,看了邮局选址问题,你更懵,请看42节!
            if (cur < ans) {
               ans = cur;
               bestChoose = leftEnd;
            }
         }
         dp[i][j] = ans;
         best[i][j] = bestChoose;
      }
   }
   return dp[N - 1][K];
}
最优解

换个思路,假设定x小时内完成需要几个画家,那么遍历一遍数组就可以求出来,如果单幅画作就已经大于x了那么返回无穷大。

那么,假设所有画作的累加和sum,k=7,先尝试目标值为sum/2,得到y=4个画家可以完成,4<7,继续修改目标值,sum/4为目标值,再计算需要的画家数y。以此类推,找到y>k的时候的目标值x2,再在x2和x1之间做2分,以此类推。找到最终的一个值即最小时间。

public static int splitArray3(int[] nums, int M) {
   long sum = 0;
   for (int i = 0; i < nums.length; i++) {
      sum += nums[i];
   }
   long l = 0;
   long r = sum;
   long ans = 0;
   while (l <= r) {
      long mid = (l + r) / 2;
      long cur = getNeedParts(nums, mid);
      if (cur <= M) {
         ans = mid;
         r = mid - 1;
      } else {
         l = mid + 1;
      }
   }
   return (int) ans;
}

public static int getNeedParts(int[] arr, long aim) {
   for (int i = 0; i < arr.length; i++) {
      if (arr[i] > aim) {
         return Integer.MAX_VALUE;
      }
   }
   int parts = 1;
   int all = arr[0];
   for (int i = 1; i < arr.length; i++) {
      if (all + arr[i] > aim) {
         parts++;
         all = arr[i];
      } else {
         all += arr[i];
      }
   }
   return parts;
}