Skip to content

DP 优化 (Optimization Techniques)

[!abstract] 当基础 DP 复杂度太高时,使用优化技巧将 O(n²) 或更高降到 O(n log n) 或 O(n)。


优化方法总览

优化方法适用场景复杂度
空间优化一维/二维滚动O(n) → O(1)
单调队列滑动窗口最值O(n)
斜率优化决策单调性O(n)
二分优化LIS 等O(n log n)
矩阵加速线性递推O(n³ log k)
分治优化凸包/四边形O(n log n)

1. 空间优化

一维滚动数组

typescript
// 原始二维 dp
const dp: number[][] = Array.from({ length: n + 1 }, () =>
  Array(m + 1).fill(0)
);
for (let i = 1; i <= n; i++) {
  for (let j = 0; j <= m; j++) {
    dp[i][j] = ...;
  }
}

// 空间优化 - 只保留需要的行
let prev: number[] = Array(m + 1).fill(0);
let curr: number[] = Array(m + 1).fill(0);

for (let i = 1; i <= n; i++) {
  [prev, curr] = [curr, prev]; // 交换
  curr.fill(0);
  for (let j = 0; j <= m; j++) {
    curr[j] = ...;
  }
}

进一步优化

typescript
// 只用一个数组(倒序遍历避免覆盖)
const dp: number[] = Array(m + 1).fill(0);

for (let i = 1; i <= n; i++) {
  // 0/1 背包:倒序
  for (let j = m; j >= weights[i]; j--) {
    dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
  }

  // 完全背包:正序
  // for (let j = weights[i]; j <= m; j++) { ... }
}

2. 单调队列优化

适用场景

  • dp[i] = min/max(dp[j] + cost(j, i)),其中 j 在滑动窗口内

经典问题:染色法站位

typescript
function shortestDistance(color: number[][]): number[] {
  const n = color[0].length;
  const INF = Infinity;
  const dp: number[][] = Array.from({ length: color.length }, () =>
    Array(n).fill(INF)
  );

  // 第一栋房子
  dp[0] = Array(n).fill(0);

  for (let i = 1; i < color.length; i++) {
    const dq: number[] = [];

    for (let j = 0; j < n; j++) {
      // 维护单调队列:dp[i-1][j] - j(斜率)
      while (dq.length > 0 && dp[i - 1][dq[dq.length - 1]] <= dp[i - 1][j]) {
        dq.pop();
      }
      dq.push(j);

      // 移除不在窗口的
      while (dq.length > 0 && dq[0] + n <= j) {
        dq.shift();
      }

      // 取最小值
      dp[i][j] = dp[i - 1][dq[0]] + Math.abs(color[i][j] - color[i - 1][dq[0]]);
    }
  }

  return dp[dp.length - 1];
}

滑动窗口模板

typescript
function monotonicQueueOptimize(dp: number[], k: number): number[] {
  const dq: number[] = []; // 存索引
  const result: number[] = [];

  for (let i = 0; i < dp.length; i++) {
    // 1. 维护单调性 - 移除比当前元素大的
    while (dq.length > 0 && dp[dq[dq.length - 1]] >= dp[i]) {
      dq.pop();
    }
    dq.push(i);

    // 2. 移除过期元素(窗口外)
    while (dq.length > 0 && dq[0] <= i - k) {
      dq.shift();
    }

    // 3. 取最值
    if (i >= k - 1) {
      result.push(dp[dq[0]]);
    }
  }

  return result;
}

3. 斜率优化

适用场景

  • dp[i] = min(dp[j] + (slope_j) * i + b_j)
  • 决策点 j 的斜率单调

公式推导

dp[i] = min(dp[j] + cost(i, j))
      = min(dp[j] + A[i] * B[j])  // 假设

设 line_j(x) = dp[j] + B[j] * x
则 dp[i] = min(line_j(A[i]))

经典问题:玩具装箱

typescript
function toyStorage(n: number, C: number, lengths: number[]): number {
  // 前缀和
  const prefix = Array(n + 1).fill(0);
  for (let i = 1; i <= n; i++) {
    prefix[i] = prefix[i - 1] + lengths[i - 1];
  }

  // 转换: x_j = prefix[j], w_i = prefix[i] + i
  // dp[i] = dp[j] + (w_i - x_j)^2
  //       = dp[j] + w_i^2 - 2*w_i*x_j + x_j^2

  const x = Array(n + 1).fill(0);
  for (let i = 1; i <= n; i++) {
    x[i] = prefix[i] + i;
  }

  const dp = Array(n + 1).fill(0);
  const dq: number[] = [0]; // 下标队列

  // 判断是否需要弹出
  const needPop = (j: number, k: number, i: number): boolean => {
    const slope = (dp: number, j: number) =>
      (dp[j] + x[j] * x[j] - dp[k] - x[k] * x[k]) / (x[j] - x[k]);
    return slope(j, dq[dq.length - 2]) <= slope(dq[dq.length - 2], i);
  };

  const calc = (j: number, i: number): number =>
    dp[j] + (x[i] - x[j]) * (x[i] - x[j]);

  for (let i = 1; i <= n; i++) {
    // 维护凸包上界
    while (dq.length >= 2 && needPop(dq[dq.length - 1], dq[dq.length - 2], i)) {
      dq.pop();
    }

    dp[i] = calc(dq[0], i);

    // 插入新点
    while (dq.length >= 2 &&
           (dp[dq[dq.length - 1]] + x[dq[dq.length - 1]] * x[dq[dq.length - 1]] - dp[dq[dq.length - 2]] - x[dq[dq.length - 2]] * x[dq[dq.length - 2]]) * (x[i] - x[dq[dq.length - 1]]) >=
           (dp[i] + x[i] * x[i] - dp[dq[dq.length - 1]] - x[dq[dq.length - 1]] * x[dq[dq.length - 1]]) * (x[dq[dq.length - 1]] - x[dq[dq.length - 2]])) {
      dq.pop();
    }
    dq.push(i);
  }

  return dp[n];
}

4. 二分优化

LIS 的二分优化

typescript
function lengthOfLIS(nums: number[]): number {
  const tails: number[] = [];

  for (const num of nums) {
    // 二分查找插入位置
    let left = 0, right = tails.length;
    while (left < right) {
      const mid = (left + right) >> 1;
      if (tails[mid] < num) left = mid + 1;
      else right = mid;
    }

    if (left === tails.length) tails.push(num);
    else tails[left] = num;
  }

  return tails.length;
}

5. 矩阵快速幂

适用场景

  • 线性递推:dp[i] = a₁dp[i-1] + a₂dp[i-2] + ... + c
typescript
function matrixMultiply(A: number[][], B: number[][]): number[][] {
  const n = A.length, m = B[0].length, k = B.length;
  const C: number[][] = Array.from({ length: n }, () => Array(m).fill(0));

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < m; j++) {
      for (let p = 0; p < k; p++) {
        C[i][j] += A[i][p] * B[p][j];
      }
    }
  }

  return C;
}

function matrixPower(A: number[][], k: number): number[][] {
  const n = A.length;
  let result: number[][] = Array.from({ length: n }, (_, i) =>
    Array(n).fill(0).map((_, j) => i === j ? 1 : 0)
  );
  let base = A;

  while (k > 0) {
    if (k & 1) result = matrixMultiply(result, base);
    base = matrixMultiply(base, base);
    k >>= 1;
  }

  return result;
}

// 斐波那契: dp[n] = dp[n-1] + dp[n-2]
// 矩阵:
// [dp[n], dp[n-1]]ᵀ = M × [dp[n-1], dp[n-2]]ᵀ
// M = [[1,1], [1,0]]

6. 分治优化

适用场景

  • DP 满足四边形不等式或单调队列单调性
  • dp[i][j] = min(dp[i][k] + dp[k+1][j] + cost(i,j))
typescript
function divideConquerDP(
  n: number,
  getCost: (l: number, r: number) => number,
  getOpt: (l: number, r: number) => number // 决策单调
): number[] {
  const dp: number[][] = Array.from({ length: n }, () => Array(n).fill(0));
  const opt: number[][] = Array.from({ length: n }, () => Array(n).fill(0));

  function compute(l: number, r: number, kLo: number, kHi: number) {
    if (l > r) return;

    const mid = (l + r) >> 1;
    let bestK = -1;
    let bestVal = Infinity;

    for (let k = kLo; k <= Math.min(mid, kHi); k++) {
      const val = dp[l][k] + dp[k + 1][mid] + getCost(l, mid);
      if (val < bestVal) {
        bestVal = val;
        bestK = k;
      }
    }

    dp[l][mid] = bestVal;
    opt[l][mid] = bestK;

    compute(l, mid - 1, kLo, bestK);
    compute(mid + 1, r, bestK, kHi);
  }

  compute(0, n - 1, 0, n - 1);
  return dp[0];
}

优化选择决策树


参见: **DP算法索引** | **基础问题**

Knowledge4J — Java 知识库