四边形优化DP 和 插头dp引入

四边形不等式优化DP:

一,介绍

四边形不等式是一种用于优化动态规划(DP)问题的数学工具,主要适用于形如 fi​=minj<i​{fj​+cost(j,i)} 的转移方程。其核心思想是通过证明代价函数满足特定的不等式性质,从而缩小状态转移时的决策变量范围,将时间复杂度从 O(n2) 优化至 O(nlogn) 或更低。

二、核心原理与关键性质

四边形不等式优化的核心依赖于以下两个关键条件:

区间包含单调性 若对于任意 (a <= b <= c <= d),代价函数满足 ({cost}(a, d) >= {cost}(b, c)),则称 ({cost}) 具有区间包含单调性。 直观理解:较大区间的代价不小于较小区间的代价(如区间越长,计算成本越高)。

四边形不等式 对于任意 (a <= b <= c <= d),代价函数满足 ({cost}(a, d) + {cost}(b, c) >= {cost}(a, c) + {cost}(b, d))。 直观理解:交叉区间的代价和不小于相邻区间的代价和(如 “长 + 短” 区间的代价和 ≥ “中 + 中” 区间的代价和)。

四边形不等式(((i,j)) 与 ((i+1,j+1)) 形式)

对于任意整数 (i leq j),代价函数 (text{cost}(i, j)) 满足:(text{cost}(i, j) + {cost}(i+1, j+1) >={cost}(i, j+1) + {cost}(i+1, j))

最重要的点:决策单调性:

三、决策单调性与优化原理

当代价函数满足上述两个条件时,DP 的决策变量 j 具有单调性,即:

设 (k(i)) 为计算 (f_i) 时的最优决策点(即使 (f_j + {cost}(j, i)) 最小的 j),则 (k(i)) 随 i 的增大而单调不减,即 (k(i-1) <= k(i))。

利用这一性质,可将状

四、优化后的算法实现步骤

预处理代价函数:验证 (text{cost}(j, i)) 是否满足四边形不等式和区间包含单调性(通常通过数学归纳法或问题性质证明)。

维护决策点数组:记录每个 i 的最优决策点 (k(i)),初始时 (k(1) = 0)(假设边界条件)。

按决策单调性枚举:计算 (f_i) 时,仅在 ([k(i-1), k(i+1)]) 范围内枚举 j,而非全区间。

更新决策点:每次计算完 (f_i) 后,更新 (k(i+1)) 的可能范围。

五,例题:石子合并:

设有 NN 堆石子排成一排,其编号为 1,2,3,…,N1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 NN 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 44 堆石子分别为 1 3 5 2, 我们可以先合并 1、21、2 堆,代价为 44,得到 4 5 2, 又合并 1、21、2 堆,代价为 99,得到 9 2 ,再合并得到 1111,总代价为 4+9+11=244+9+11=24;

如果第二步是先合并 2、32、3 堆,则代价为 77,得到 4 7,最后一次合并代价为 1111,总代价为 4+7+11=224+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式

第一行一个数 NN 表示石子的堆数 NN。

第二行 NN 个数,表示每堆石子的质量(均不超过 10001000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

这里的cost是sum(i,j),那么一定有sum(i,j+1)+sum(i+1,j)>=sum(i,j)+sum(i+1,j+1);

可以优化,

对于一个区间len,他们的可参考区间是len-1的区间

#include <iostream>

#include <vector>

#include <algorithm>

const int INT_MAX=0x3f3f3f3f;

using namespace std;

int stoneMergeOptimized(vector<int>& stones) {

    int n = stones.size();

    if (n == 0) return 0;

    

    // 计算前缀和数组

    vector<int> prefixSum(n + 1, 0);

    for (int i = 1; i <= n; i++) {

        prefixSum[i] = prefixSum[i - 1] + stones[i - 1];

    }

    

    // 初始化dp数组和决策点数组

    vector<vector<int>> dp(n, vector<int>(n, 0));

    vector<vector<int>> kTable(n, vector<int>(n, 0));

    

    // 初始化长度为1的区间

    for (int i = 0; i < n; i++) {

        dp[i][i] = 0;

        kTable[i][i] = i;

    }

    

    // 枚举区间长度l从2到n

    for (int l = 2; l <= n; l++) {

        for (int i = 0; i <= n - l; i++) {

            int j = i + l - 1;

            dp[i][j] = INT_MAX;

            int sum = prefixSum[j + 1] - prefixSum[i];

            

            // 决策点范围优化:k∈[kTable[i][j-1], kTable[i+1][j]]

            int startK = kTable[i][j - 1];

            int endK = kTable[i + 1][j];

            if (startK < i) startK = i;

            if (endK > j - 1) endK = j - 1;

            

            for (int k = startK; k <= endK; k++) {

                int current = dp[i][k] + dp[k + 1][j] + sum;

                if (current < dp[i][j]) {

                    dp[i][j] = current;

                    kTable[i][j] = k;

                }

            }

        }

    }

    

    return dp[0][n - 1];

}

int main() {

    vector<int> stones ;

    int n;

    cin>>n;

    for(int i=0;i<n;i++){

        int tmp;

        cin>>tmp;

        stones.push_back(tmp);

    }

    cout  << stoneMergeOptimized(stones) << endl;

    return 0;

}

例题2:诗人小G

小 GG 是一个出色的诗人,经常作诗自娱自乐。

但是,他一直被一件事情所困扰,那就是诗的排版问题。

一首诗包含了若干个句子,对于一些连续的短句,可以将它们用空格隔开并放在一行中,注意一行中可以放的句子数目是没有限制的。

小 GG 给每首诗定义了一个行标准长度(行的长度为一行中符号的总个数),他希望排版后每行的长度都和行标准长度相差不远。

显然排版时,不应改变原有的句子顺序,并且小 GG 不允许把一个句子分在两行或者更多的行内。

在满足上面两个条件的情况下,小 GG 对于排版中的每行定义了一个不协调度,为这行的实际长度与行标准长度差值绝对值的 PP 次方,而一个排版的不协调度为所有行不协调度的总和。

小 GG 最近又作了几首诗,现在请你对这几首诗进行排版,使得排版后的诗尽量协调(即不协调度尽量小),并把排版的结果告诉他。

输入格式

第一行包含一个整数 TT,表示诗的数量,接下来是 TT 首诗,每首诗是一组数据。

每组数据的第一行包含三个整数 N,LN,L 和 PP,其中 NN 表示这首诗句子的数目,LL 表示这首诗的行标准长度,PP 的含义参考问题描述。

从第二行开始,每行一个句子,句子由英文字母、数字、标点符号等符号组成(ASCII 码 33∼12733∼127,但不包含 -)。

输出格式

对于每组测试数据,若最小的不协调度不超过 10181018,则第一行为一个数,表示不协调度。接下来若干行,表示你排版之后的诗。注意:在同一行的相邻两个句子之间需要用一个空格分开。

如果有多个可行解,它们的不协调度都是最小值,则输出任意一个解均可。(本题有 special judge)(由于本题数据量大,展示标准答案时,不展示可行解)

若最小的不协调度超过 10181018,则输出 Too hard to arrange。

每组测试数据结束后输出 --------------------,共 2020 个 -,- 的 ASCII 码为 4545,请勿输出多余的空行或者空格。

#include <cstdio>

#include <cstring>

typedef long double ll;

const int N = 100005;

int n, L, P;

char str[N][31];

int s[N], g[N], res[N];

ll f[N];

// 队列

struct Node

{

    // 区间 [l, r] 当前的决策都是 x

    int l, r, x;

} q[N];

int hh, tt;

ll min(ll a, ll b) {return a < b ? a : b;}

// 返回将第 i 句诗放到第 j 句诗所在行后,前 i 句诗的不协调度和

ll w(int j, int i)

{

    ll v = 1, x = s[i] - s[j - 1] - 1 - L;

    for (int k = P; k; k >>= 1, x *= x)

        if (k & 1) v *= x;

    return (v > 0 ? v : -v) + f[j - 1];

}

// 将 i 插入队列

void insert(int i)

{

    int p = -1; // 存 i 应该插入的位置

    while (hh <= tt)

        // 当队列中有元素,并且 i 比队头的 l 的决策更优,则将队头元素删除,取队头的 l 做 i 插入的位置

        if (w(i, q[tt].l) <= w(q[tt].x, q[tt].l)) p = q[tt -- ].l;

        else    break;

    // 如果删完之后,如果队头元素的 r 的的决策不比决策 i 更优,则在队头的 [l, r] 中二分查找

    if (w(i, q[tt].r) < w(q[tt].x, q[tt].r))

    {

        int l = q[tt].l, r = q[tt].r, mid;

        while (l < r)

        {

            mid = l + r >> 1;

            // 如果 i 做 mid 的决策比队头的 x 做 mid 的决策更优,则应该往前找,让 r = mid

            if (w(i, mid) <= w(q[tt].x, mid)) r = mid;

            else    l = mid + 1; // 否则往后找

        }

        // 二分之后 r 就是我们要找的位置,即 r 前面的所有决策都比 i 更优,r 及其后面 i 更优

        q[tt].r = r - 1, p = r;

    }

    // 如果 p 不等于 -1,说明 i 比原来队头的某个决策更优,更新了队列,那么就将 p 及以后的决策都改为 i

    if (~p) q[ ++ tt] = {p, n, i};

}

int main()

{

    int task;

    for (scanf("%d", &task); task -- ;)

    {

        scanf("%d %d %d", &n, &L, &P);

        for (int i = 1; i <= n; i ++ )

        {

            scanf("%s", str[i]);

            s[i] = s[i - 1] + strlen(str[i]) + 1;

        }

        // 将队列清空,并将 1 ~ n 的最优决策都设为 0

        hh = tt = 0, *q = {1, n, 0};

        for (int i = 1; i <= n; i ++ )

        {

            insert(i);            // 在队列中插入 i

            g[i] = q[hh].x, f[i] = w(g[i], i); // f[i] 的最优决策即为队头

            while (hh <= tt && q[hh].r <= i) hh ++ ; // 队列中所有 1 ~ i 中的元素直接去掉

            q[hh].l = i + 1;      // 将队列中最后一个元素中,1 ~ i 中的元素去掉

        }

        if (f[n] > 1e18) puts("Too hard to arrange");

        else

        {

            printf("%lldn", (long long) f[n]); // 输出答案

            // 输出决策。res[i] 表示第 i 句诗所在行中,末尾诗句是第几句

            for (int i = n; i; i = g[i] - 1) res[g[i]] = i;

            for (int i = 1; i <= n; i = res[i] + 1)

            {

                for (int j = i; j <= res[i]; j ++ )

                {

                    printf("%s", str[j]);

                    // 注意不要输出多余空格

                    if (j != res[i]) putchar(32);

                }

                putchar(10);

            }

        }

        for (int i = 20; i -- ; putchar(45));

        putchar(10);

    }

    return 0;

}

插头 DP 简介

插头 DP 是一种用于解决棋盘路径、回路等状态压缩动态规划问题的方法。核心思想是通过状态压缩记录棋盘上分界线(通常是从左到右、从上到下扫描棋盘时划分已处理和未处理部分的线)上插头(可以理解为路径通过分界线的连接点)的状态,利用动态规划递推求解。

各情况分析

1-(i, j) 是障碍物

此时不能有路径经过这个格子,所以如果((i, j))是障碍物,那么对于任何插头状态s,(f[i][j][s]=0) 。因为不可能存在合法路径经过障碍格来满足题目要求。

2-(i, j) 两个边都是 0

当左边插头和上边插头都是0(即没有插头连接过来)时,若当前格子不是回路的起点,这种情况是不合法的,(f[i][j][s]=0) 。若当前格子是回路起点(一般在处理第一行第一列等特殊位置时判断),可以新开启一条路径,后续根据状态转移规则继续递推(只能是下和右都有)。

3-左非 0 上 0

左边有插头连接过来(插头状态为1或2 ),上边没有插头。此时可以将左边的插头延伸到当前格子,然后根据延伸后的插头状态更新(f[i][j][s]) 。比如左边插头为1 ,延伸到当前格子后,若当前格子右边没有其他插头连接,那么右边插头变为1 ,同时更新状态对应的方案数。当然可以向下更新

4-上非 0 左 0

与 “左非 0 上 0” 类似,只是插头来源是上边。将上边的插头延伸到当前格子,根据延伸后的插头状态更新(f[i][j][s]) 。例如上边插头为2 ,延伸后若当前格子左边没有其他插头连接,左边插头变为2 ,并更新方案数。当然另外一个方向同理

5-左 1 上 2

只有右下角的格子可能有这个情况

6-左 2 上 1

左边插头为2 ,上边插头为1 ,说明有两条不同的路径从不同方向过来。需要按照路径连接规则将它们合理连接起来,更新插头状态(比如将这两个插头合并成一个新的状态,类似于括号匹配中不同括号的合并),并更新(f[i][j][s]) 。

都是 1

同 情况,两条路径在当前格子相遇,将它们合并成一条路径,更新插头状态并更新(f[i][j][s]) 。这里的合并操作要遵循插头状态的编码规则,比如将两个1编码成一个表示合并后路径的新状态。

都是 2

与 “都是 1” 类似,两条路径在当前格子相遇,按照规则合并路径,更新插头状态,进而更新(f[i][j][s]) 。

整体求解流程

初始化边界条件,比如第一行第一列等特殊位置的状态。

按照从左到右、从上到下的顺序遍历棋盘的每个格子。

对于每个格子,根据上述不同情况进行状态转移,更新(f[i][j][s]) 。

遍历完整个棋盘后,最终的回路数量通常可以从最后一行最后一列对应的合法状态的f值中获取(具体取决于状态编码和问题设定)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值