Skip to content

凸优化与决策单调性

最近遇到了一些决策单调性和闵可夫斯基和的题,就慢慢放在这里,顺便学习一下相关的国家集训队论文。

CF1601C

性质:

将一个数 \(x\) 插入到一个数组任意的位置,使新数组逆序对最少的位置(若有多个位置,取最小的位置)\(p[x]\) 关于 \(x\) 单调不减。

证明:记 \(w(i,j)\) 表示把 \(i\) 插入到数组的第 \(j\) 个数后新增的逆序对数,\(p[x]=j\) 表示将 \(x\) 的最优位置是数组的第 \(j\) 个数后。

先证明一个引理,也就是四边形不等式:

\(w(i,j) + w(i+1,j+1) \geq w(i,j+1)+w(i+1,j)\)

上式等价于 \((a[j]<i+1)-(a[j]>i+1) \geq (a[j] < i) - (a[j] > i)\)

分类讨论 \(a[j]\) 的取值可以得证。

对两个维度分别归纳可以得到完整的四边形不等式 假设 \(a \leq b\)\(c \leq d\) 。则有:

\(w(a,c)+w(b,d) \geq w(a,d)+w(b,c)\)

考虑反证法:假设存在 \(x<y\) ,使得 \(p[x]>p[y]\)

根据 \(p[x]\) 的定义,可以得到: \(w(x,p[x])<w(x,p[y])\)\(w(y,p[y]) \leq w(y,p[x])\)

两式相加有:\(w(x,p[x])+w(y,p[y])<w(x,p[y])+w(y,p[x])\) 。这和四边形不等式矛盾,得证!

思路:

这个性质其实就是决策单调性本身,它告诉我们直接插入最优决策点不会让 \(b\) 数组之间的数产生逆序对,是最优的策略。因为这个题在相邻决策点之间的转移很简单,所以可以分治求解。

具体地,考虑 \(sol(l,r,x,y)\) 表示 \([l,r]\) 的最优决策点都在 \([x,y]\) 之间,令 \(mid =(l+r)/2\) 。暴力求出 \(mid\) 的最优决策点 \(pmid\) 。递归到 \(sol(l,mid-1,x,pmid)\)\(sol(mid+1,r,pmid,y)\)

求出所有的最优的决策点后构建完整的序列求逆序对即可。

分治结构一共有 \(logm\) 层,每层的复杂度都是 \(O(n)\) 。复杂度为 \(O(nlogm)\)

代码
#include<bits/stdc++.h>
#define ll long long

using namespace std;
std::mt19937 rng(std::chrono::steady_clock::now().time_since_epoch().count());

const int M = 1e6;

int n, m, a[M + 10], b[M + 10],  p[M + 10];


void sol(int l, int r, int x, int y) {
    if (l > r)return;
    int mid = l + r >> 1;
    int sum = 0, pos = x, ans = 0;
    //[1,x) [x,n+1)
    for (int i = x + 1; i <= y; i++) {
        sum += (a[i - 1] > b[mid]);
        sum -= (a[i - 1] < b[mid]);
        if (sum < ans)pos = i, ans = sum;
    }
    p[mid] = pos;
    sol(l, mid - 1, x, pos);
    sol(mid + 1, r, pos, y);
}

template <typename T>
struct Fenwick {
    int n;
    vector<T> a;

    Fenwick(int n_ = 0) {
        init(n_);
    }

    void init(int n_) {
        n = n_;
        a.assign(n, T{});
    }

    void add(int x, const T &v) {
        for (int i = x + 1; i <= n; i += i & -i) {
            a[i - 1] = a[i - 1] + v;
        }
    }
    //[0,x)
    T sum(int x) {
        T ans{};
        for (int i = x; i > 0; i -= i & -i) {
            ans = ans + a[i - 1];
        }
        return ans;
    }

    T query(int l, int r) {
        return sum(r) - sum(l);
    }

    //[0,x)和小于等于k最大的x
    int select(const T &k) {
        int x = 0;
        T cur{};
        for (int i = 1 << __lg(n); i; i /= 2) {
            if (x + i <= n && cur + a[x + i - 1] <= k) {
                x += i;
                cur = cur + a[x - 1];
            }
        }
        return x;
    }
};

void solve()
{

    cin >> n >> m;
    vector<int>v;
    for (int i = 1; i <= n; i++)cin >> a[i], v.push_back(a[i]);
    for (int i = 1; i <= m; i++)cin >> b[i], v.push_back(b[i]);
    sort(v.begin(), v.end());
    sort(b + 1, b + m + 1);
    int c = v.size();
    for (int i = 1; i <= n; i++)a[i] = lower_bound(v.begin(), v.end(), a[i]) - v.begin();
    for (int i = 1; i <= m; i++)b[i] = lower_bound(v.begin(), v.end(), b[i]) - v.begin();
    sol(1, m, 1, n + 1);

    ll res = 0;
    Fenwick<int>tr(c);
    for (int i = 1, j = 1; i <= n + 1; i++) {
        while (j <= m && p[j] <= i)res += tr.query(b[j] + 1, c), tr.add(b[j], 1),j++;
        if (i <= n)res += tr.query(a[i] + 1, c), tr.add(a[i], 1);
    }
    cout << res << '\n';
}

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1; cin >> t;
    while (t--)solve();
    return 0;
}

洛谷 P4983

简要题意

将序列分成 \(m\) 段,每段的权值为 \((1+\sum_{i=1}^{n}a_i)^2\) ,最小化权值和。

思路分析

先简单介绍下 WQS 二分:当我们确认一个函数是关于某个变量的凸函数时,我们可以通过 WQS 二分获取某点处的值。

具体方法为:每次二分一个斜率,找到该斜率的切线与凸函数的切点横坐标, 再根据切点横坐标与我们希望的横坐标的大小关系调整二分区间。二分找到合适的斜率后最后再跑一次求切点的过程即可找出点值。

理由如下:不妨设当前二分的斜率为 \(k\) ,找切点本质上就是找一点 \((x,y)\) 使得 \(y-kx\) 最大或最小。

对于本题,记 \(dp[x]\) 为划分为 \(x\) 段的最小代价,可以证明其是关于 \(x\) 的凸函数,那我们就是希望获取它在 \(m\) 处的点值。

对于如何找 \(x\) 使得 \(dp[x]-kx\) 最小,我们把后面那部分均摊到每一段中,也就是让每一段的代价变为 \((1+\sum_{i=1}^{n}a_i)^2-k\) 。 在这种情况下,记 \(f[i]\) 表示前 \(i\) 个数的所有划分方案中的最小代价,\(g[i]\) 表示相应的划分段数(有多个取最小),那么 \(g[n]\) 就是我们要找的切点横坐标。 同时最后也可以通过 \(f[i]\) 确定最开始的代价为: \(f[i]+k*g[i]\)

考虑 \(f[i]\) 的转移,枚举上一段的终点 \(j(0 \leq j < i)\) ,其中 \(j\)\(0\) 表示不存在上一段,得 \(f[i]=min(f[j]+(1+s[i]-s[j])^2+k)\) ,这是个经典的斜率优化 dp 。 于是问题就解决了。

最后提一下为什么可以在整数域上二分,因为相邻的两点横坐标之差为 \(1\) ,这就保证了这条线段的斜率为整数,于是我们总能找到一条斜率为整数的切线符合我们的条件。

实际上要保证多种方案选择最小的点有很多需要讨论的地方,所有下面这份代码还不算太完善。

代码
#include<bits/stdc++.h>
#define ll long long

using namespace std;
std::mt19937 rng(std::chrono::steady_clock::now().time_since_epoch().count());

const int N = 2e5 + 10;

int n, m, a[N], g[N];
int q[N], hh, tt;
ll f[N], s[N];

ll c(int x) {

    return f[x] + s[x] * s[x] - 2 * s[x];
}

long double slope(int i, int j) {
    return (long double)(c(i) - c(j)) / (s[i] - s[j]);
}

bool check(ll k) {
    hh = tt = q[0] = 0;
    for (int i = 1; i <= n; i++) {
        while (hh > tt && slope(q[tt], q[tt + 1]) <= 2 * s[i] )tt++;
        int j = q[tt];
        f[i] = f[j] + (1 + s[i] - s[j]) * (1 + s[i] - s[j]) - k;
        g[i] = g[j] + 1;
        while (hh > tt && slope(i, q[hh]) < slope(q[hh], q[hh - 1]))hh--;
        q[++hh] = i;
    }
    return g[n] >= m;
}

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)cin >> a[i];
    for (int i = 1; i <= n; i++)s[i] = s[i - 1] + a[i];
    ll l = -1e18, r = 1e18;
    while (l < r) {
        ll mid = l + (r - l) / 2;
        if (check(mid))r = mid;
        else l = mid + 1;
    }
    check(l);

    cout << f[n] + m * l << '\n';
}

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    while (t--)solve();
    return 0;
}