C区间 DP

环形区间 DP

断环为链·能量项链

本课摘要

环形区间 DP课程回答“环形区间怎样通过复制序列转化为链形区间”。内容以断环为链·能量项链为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断环形区间 DP的适用条件与状态边界
  • 围绕“断环为链·能量项链”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

当石子摆成一个环

上一节的石子排成一条链:两端是「头」和「尾」,谁也不挨着谁。可石子合并的原题(P1880)里,石子其实摆成一个环——第 n−1n-1 堆与第 00 堆也相邻,也能合并。规则不变:每次并相邻两堆、代价为两堆之和,直到剩一堆,求最小(或最大)总代价。

第 0 堆3第 1 堆9第 2 堆3第 3 堆4环和链形唯一的差别· 第 0 堆与第 3 堆也相邻,可以合并· 最后剩的那一堆,起点断在哪里不定
环形石子:n 堆首尾相接,第 0 堆与第 n-1 堆之间多出一条「链形没有」的相邻边——这正是环与链唯一的差别。

差别虽小,却不能直接照搬链形。链形 dp[1][n]dp[1][n] 默认最后剩下的那堆断在第 11 堆左侧;但在环上,「最后剩的那堆从哪里断开」是自由的——可以从任意一堆起、绕一圈回来。以 a=[3,9,3,4]a=[3,9,3,4] 为例:当成直链算得 dp[0][3]=38dp[0][3]=38;可若允许「先并第 3 堆与第 0 堆」(环上它们相邻),从第 11 堆起绕一圈只需 3636。那条多出来的边,能让合并更省。

也别想着「枚举每个断点,各跑一遍链形 DP」——那要跑 nn 遍、白白多花一个 nn 倍。有没有办法一次把所有断法都算进去?有,而且极简洁:断环为链。

断环为链:复制一倍,环上任一圈都成了链上一段

核心一招:把石子数组复制一倍,首尾拼成一条长度 2n2n 的链a2[0…2n−1]a2[0\ldots 2n-1],其中 a2[i]=a[i mod n]a2[i]=a[i\bmod n]。这样一来,环上从任意堆起、绕一整圈的那 nn 堆,在这条 2n2n 链里都恰好是一段连续区间 [i, i+n−1][i,\ i+n-1]。原本「绕过尾首」的麻烦相邻边,被复制的那半段抹平成了普通的链内相邻。

3934✂任选一处剪开复制一倍3091324334953647原始 n 堆复制的 n 堆窗口 [1, 4]:从第 1 堆起、绕过尾首的一整圈
从任一处剪开环、把 n 堆复制一倍接成 2n 链。环上「从第 1 堆起绕一圈」= 链上连续区间 [1,4]——绕过尾首的相邻,变成了链内相邻。

于是环形问题被化归成链形:在 a2a2 上跑一模一样的区间三角表(状态、转移、按长度递推,全部照搬上一节),只是网格从 n×nn\times n 变成 2n×2n2n\times 2n:

dp[l][r]=min⁡l≤k≤r−1(dp[l][k]+dp[k+1][r])+sum(a2[l..r])dp[l][r]=\min_{l\le k\le r-1}\big(dp[l][k]+dp[k+1][r]\big)+\mathrm{sum}(a2[l..r])

算完之后,环形答案不是某一格,而是枚举所有起点 i=0,1,…,n−1i=0,1,\ldots,n-1,在这 nn 个「整圈窗口」里取最优:

ans=min⁡0≤i<ndp[i][i+n−1]\mathrm{ans}=\min_{0\le i<n} dp[i][i+n-1]

为什么长度只需枚举到 nn、窗口长度恰取 nn?因为一整圈正好 nn 堆:长度小于 nn 合不完,长度大于 nn 会让某堆被数两次(既在原段、又在复制段),非法。取 max⁡\max 就把 min⁡\min 换成 max⁡\max,一字不改。

本质

环形区间 DP = 链形区间 DP + 一层「断点」枚举,而这层枚举被「复制一倍成 2n2n 链」悄悄吸收进了同一张三角表里。诀窍在于:环上任一条连续弧,在 2n2n 链上都能找到一段等价的连续区间——于是「从哪里断」不必外层重复跑,只需最后在 nn 个长度为 nn 的窗口里取最优。复杂度仍是三层循环,O((2n)3)=O(n3)O((2n)^3)=O(n^3)。

跟着算一遍:a=[3,9,3,4] 的那个更省的窗口

仍用 a=[3,9,3,4]a=[3,9,3,4]。复制一倍得 a2=[3,9,3,4, 3,9,3,4]a2=[3,9,3,4,\ 3,9,3,4](下标 0…70\ldots7),前缀和 pre=[0,3,12,15,19,22,31,34,38]pre=[0,3,12,15,19,22,31,34,38]。答案落在起点 11 的窗口 [1,4][1,4](对应 a2[1..4]=[9,3,4,3]a2[1..4]=[9,3,4,3],即环上从第 1 堆绕一圈)。把这个窗口按长度算出来:

0
长度 1:对角线全 00。长度 2(区间和即两堆之和):dp[1][2]=12dp[1][2]=12(9+39{+}3)、dp[2][3]=7dp[2][3]=7、dp[3][4]=7dp[3][4]=7。
1
长度 3,看 [1,3][1,3](区间和 1616):k=1k=1→0+7=70+7=7,k=2k=2→12+0=1212+0=12。取小 77,加 1616 → dp[1][3]=23dp[1][3]=23。同理 dp[2][4]=17dp[2][4]=17。
2
长度 4(整圈窗口),看 [1,4][1,4](区间和 1919):k=1k=1→0+17=170+17=17,k=2k=2→12+7=1912+7=19,k=3k=3→23+0=2323+0=23。取小 1717,加 1919 → dp[1][4]=36dp[1][4]=36。
✓
扫窗取优:四个窗口 dp[0][3]=38, dp[1][4]=36, dp[2][5]=36, dp[3][6]=38dp[0][3]=38,\ dp[1][4]=36,\ dp[2][5]=36,\ dp[3][6]=38,最小 3636——比朴素直链的 3838 省 22。这 22,就是那条「尾首相邻边」买来的。
下面的演示把 2n2n 链的三角表按长度一层层填满,末帧再并排点亮 nn 个整圈窗口、圈出最优的那个。改改环上数值,看答案落到哪个起点。

看 2n 链的三角表长出来

环上石子(首尾相邻 · 可改每堆数值 · 3~4 堆)
0
石子数 a
3
1
石子数 a
9
2
石子数 a
3
3
石子数 a
4
断环为链后在 2n = 8 长的链上填三角表,扫 4 个长度 4 的窗口 → 环形最小合并代价 = 36。默认 a=[3,9,3,4] 时答案 36,落在起点 1 的窗口(即环上从第 1 堆断开、绕过尾首那一整圈), 比朴素当成直链的 dp[0][3]=38 更省——这正是「环」多出来的那条边带来的收益。
r=0
r=1
r=2
r=3
r=4
r=5
r=6
r=7
l=0
l=1
l=2
l=3
l=4
l=5
l=6
l=7
0
·
·
·
·
·
·
·
·
0
·
·
·
·
·
·
·
·
0
·
·
·
·
·
·
·
·
0
·
·
·
·
·
·
·
·
0
·
·
·
·
·
·
·
·
0
·
·
·
·
·
·
·
·
0
·
·
·
·
·
·
·
·
0
当前计算 依赖来源 被选转移 已确定
a2[i]=a[i mod n],dp[l][l]=0a2[i]=a[i\bmod n],\quad dp[l][l]=0
断环为链:把 4 堆复制一倍成长度 2n=8 的链;对角线 dp[l][l]=0。
已暂停,第 1 步,共 20 步,1 倍速

为什么要枚举窗口:一张图看断点如何平移

断环为链的几何直觉是:nn 个「整圈窗口」[0,n−1],[1,n],…,[n−1,2n−2][0,n-1],[1,n],\ldots,[n-1,2n-2] 在 2n2n 链上逐格右移,每一个对应「从某堆断开」的一种合并方案。它们覆盖的都是环上同一圈的 nn 堆,只是起止不同——所以答案要在它们之间取最优,缺一不可。

01234567dp[0][3]起点 0dp[1][4]起点 1dp[2][5]起点 2dp[3][6]起点 3环形答案 = min / max 这 n 个窗口值(长度超过 n 的窗口会让某堆被合并两次,非法,不枚举)
2n 链上,n 个长度为 n 的窗口逐行下移(起点 0→n-1);环形答案 = 这 n 个 dp[i][i+n-1] 的最优。长度超过 n 会重复计堆,非法。

记死这套「复制一倍 + 三层循环 + 扫窗」骨架——几乎所有环形合并/区间题都用它:

for i = 0 … n-1:  a2[i+n] = a[i]          // ★复制一倍,链长 2n
for 长度 len = 2 … n:                      // 只需到 n(一整圈)
  for 左端点 l = 0 … 2n-len:
    r = l + len - 1
    for 分割点 k = l … r-1:
      dp[l][r] = min( dp[l][r], dp[l][k] + dp[k+1][r] + sum(a2[l..r]) )
ans = min over i∈[0,n-1] of dp[i][i+n-1]   // ★扫 n 个整圈窗口取优

换个断点,窗口就平移:环↔链展开

上面是「填表」视角;再换个「展开」视角把直觉坐实。下面的互动让你亲手选断点:环从那里剪开、展成 2n2n 直链,对应的长度 nn 窗口随之在链上整体平移。切几个断点,感受「同一圈、不同起止」,以及为何单看一个 dp[0][n−1]dp[0][n-1] 会漏掉更优解。

选一个断点,看环怎样展开成 2n 直链
03192334✂起点 1展开3091324334953647窗口 dp[1][4]:从起点 1 起的一整圈(4 堆)
换个断点,窗口就在 2n 链上整体平移一格,覆盖的仍是环上的同一圈 4 堆、只是起止不同。因此环形答案不能只看一个 dp[0][3]——要把这 4 个平移窗口都试一遍,取最优。链一旦复制成 2n,任何一种“从哪儿断”都变成链上一个现成的连续区间,环形问题就此化归为已会的链形区间 DP。

两个常见坑

其一,长度别超过 nn。在 2n2n 链上若枚举到长度 >n>n 的区间,会把某堆石子数两遍,答案偏大且无意义——外层 lenlen 只跑到 nn 即可。
其二,取 min⁡\min 时下三角别参与、初值要设对。ff 初值 +∞+\infty、gg 初值 −∞-\infty,且只在合法上三角(l≤rl\le r)转移。若像能量项链那样代价可能为负(如三元乘积含负数),求最小时还要留意「负负得正」,别漏候选。

例题

P1880[NOI1995] 石子合并NOI1995提高+/省选-
题意
nn 堆石子摆成一环,每次合并相邻两堆、代价为两堆之和,直到并成一堆。分别求最小与最大总代价。
对应关系
环形区间 DP 的标准模板题,且一题双问:断环为链(复制一倍成 2n2n),在链上用两张表 ff(最小)、gg(最大)并行跑同一套三层循环,最后各扫 nn 个整圈窗口取优。本页从头到尾就是它。
为什么选它
它把环形处理与min/max 双问两个要点压在一题里,是检验「断环为链是否真会」的试金石:f/g[l][r]f/g[l][r] 的转移与链形完全一致,唯一新增的就是「复制一倍 + 扫窗」这两处——把这两处默写下来,环形区间 DP 就到手了。
转移 · 复杂度
f/g[l][r]=optk(f/g[l][k]+f/g[k+1][r])+sum(l,r)f/g[l][r]=\mathrm{opt}_k(f/g[l][k]+f/g[k+1][r])+\mathrm{sum}(l,r),链长 2n2n、外层长度到 nn、扫窗 i∈[1,n]i\in[1,n];时间 O(n3)O(n^3)。
参考代码(断环为链 · 双问并行 · 扫窗)
#include <iostream>
using namespace std;

const int INF = 0x3f3f3f3f;
int a[205];                       // ★断环为链:a[i] 与 a[i+n] 同值,链长 2n
int pre[205];                     // 前缀和,sum(l..r) = pre[r] - pre[l-1]
int f[205][205];                 // 最小合并代价
int g[205][205];                 // 最大合并代价

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        a[i + n] = a[i];          // ★复制一倍,接成长度 2n 的链
    }
    for (int i = 1; i <= 2 * n; i++)
        pre[i] = pre[i - 1] + a[i];

    for (int len = 2; len <= n; len++)          // 区间长度只需到 n(一整圈)
        for (int l = 1; l + len - 1 <= 2 * n; l++)
        {
            int r = l + len - 1;
            int s = pre[r] - pre[l - 1];        // 本区间合并追加的代价
            f[l][r] = INF;
            g[l][r] = -INF;
            for (int k = l; k <= r - 1; k++)    // 枚举分割点 k
            {
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s);
                g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s);
            }
        }

    int mn = INF, mx = -INF;
    for (int i = 1; i <= n; i++)                 // ★枚举 n 个长度为 n 的窗口
    {
        mn = min(mn, f[i][i + n - 1]);
        mx = max(mx, g[i][i + n - 1]);
    }
    cout << mn << endl;                          // 一题双问:最小、最大
    cout << mx << endl;
    return 0;
}
P1063[NOIP2006 提高组] 能量项链NOIP2006 提高组普及+/提高
题意
nn 颗珠子串成一环,每颗珠有头、尾两个标记,相邻珠共享标记。合并相邻两珠 (i,j)(i,j) 与 (j,k)(j,k) 得新珠 (i,k)(i,k),释放能量 ei⋅ej⋅eke_i\cdot e_j\cdot e_k。求把整串合成一颗珠能释放的最大总能量。
为什么选它
环形区间 DP 的经典进阶:合并代价从「区间和」升级成相邻三元乘积 head⋅mid⋅tail\mathrm{head}\cdot\mathrm{mid}\cdot\mathrm{tail}。状态改成按标记划分——f[i][j]f[i][j] 表示标记 i..ji..j 之间的珠子合成一颗的最大能量,枚举最后一并处的中间标记 kk,追加 eiekeje_i e_k e_j。断环为链的处理与石子合并一模一样,正好训练「同一套环形骨架、换一种代价函数」的迁移。
换个视角
这里 dpdp 的下标是标记(隔板)而非珠子:长度 len\mathrm{len} 的区间 [i,j][i,j] 含 j−ij-i 颗珠,端点标记 ei,eje_i,e_j 是合成后新珠的两头。样例 e=[2,3,5,10]e=[2,3,5,10] 的答案是 710710。
转移 · 复杂度
f[i][j]=max⁡i<k<j(f[i][k]+f[k][j]+eiekej)f[i][j]=\max_{i<k<j}\big(f[i][k]+f[k][j]+e_i e_k e_j\big),标记链长 2n2n,扫 len=n\mathrm{len}=n 的窗口;时间 O(n3)O(n^3)。
参考代码(断环为链 · 三元乘积)
#include <iostream>
using namespace std;

int e[205];                       // 珠子上的标记值,断环为链后长 2n
long long f[205][205];           // f[i][j]:把标记 i..j 之间的珠子合成一颗的最大释放能量

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> e[i];
        e[i + n] = e[i];          // ★复制一倍
    }

    long long ans = 0;
    for (int len = 2; len <= n; len++)          // len = 相邻标记跨度,含 len 个原珠首尾标记
        for (int i = 1; i + len <= 2 * n; i++)
        {
            int j = i + len;                    // 合成后新珠的两端标记 e[i]、e[j]
            for (int k = i + 1; k < j; k++)     // 枚举最后一次并珠处的中间标记 k
            {
                long long v = f[i][k] + f[k][j] + (long long)e[i] * e[k] * e[j];
                f[i][j] = max(f[i][j], v);       // ★最后一并释放 head*mid*tail
            }
            if (len == n)                        // 一整圈:更新答案
                ans = max(ans, f[i][j]);
        }

    cout << ans << endl;
    return 0;
}

练习

P1043[NOIP2003 普及组] 数字游戏环形 + 分段区间 DP:数字排成环,分成 m 段,各段和对 10 取模后相乘,求最大/最小。断环为链(复制一倍)后枚举起点,状态加一维段数:dp[l][r][t] = 区间 [l,r] 分成 t 段的最优,转移枚举最后一段的分割点。取模后可能为负,求最小值别漏「负负得正」,最大最小两张表分开跑。在洛谷打开
P2426删数区间合并变形:把相邻或首尾的数按规则合并/删除,代价与两端点相关。设 dp[l][r] 为处理区间 [l,r] 的最优值,枚举分割点或枚举“最后删哪个”转移;按区间长度由短到长递推,注意端点代价的定义与边界。在洛谷打开
想更直观地感受「从哪儿断、绕哪一圈」?回 C 部分页的互动里亲手挑一个断点与合并顺序,再看 DP 给出的最优圈。

已进入 环形区间 DP · 区间 DP · DP大师