P5665 [CSP-S2019] 划分 做题记录

题目传送门

题目描述

2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 \(n\) 组数据,数据从 \(1 \sim n\) 编号,\(i\) 号数据的规模为 \(a_i\)

小明对该题设计出了一个暴力程序,对于一组规模为 \(u\) 的数据,该程序的运行时间\(u^2\)。然而这个程序运行完一组规模为 \(u\) 的数据之后,它将在任何一组规模小于 \(u\) 的数据上运行错误。样例中的 \(a_i\) 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。

也就是说,小明需要找到一些分界点 \(1 \leq k_1 \lt k_2 \lt \cdots \lt k_p \lt n\),使得

\[\sum_{i=1}^{k_1} a_i \leq \sum_{i=k_1+1}^{k_2} a_i \leq \cdots \leq \sum_{i=k_p+1}^{n} a_i \]

注意 \(p\) 可以为 \(0\) 且此时 \(k_0 = 0\),也就是小明可以将所有数据合并在一起运行。

小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化

\[(\sum_{i=1}^{k_1} a_i)^2 + (\sum_{i=k_1+1}^{k_2} a_i)^2 + \cdots + (\sum_{i=k_p+1}^{n} a_i)^2 \]

小明觉得这个问题非常有趣,并向你请教:给定 \(n\)\(a_i\),请你求出最优划分方案下,小明的程序的最小运行时间。

数据范围

所有测试点满足:\(type \in \{0,1\}\)\(2 \leq n \leq 4 \times 10^7\)\(1 \leq a_i \leq 10^9\)\(1 \leq m \leq 10^5\)\(1 \leq l_i \leq r_i \leq 10^9\)\(0 \leq x,y,z,b_1,b_2 \lt 2^{30}\)

思路

不难想到 \(\Theta (n^3)\) 的 dp:对每个点 \(i\),记录 \(d_{i,j}\) 表示以 \(i\) 结尾,最长一段为 \((j, i]\) 的费用。\(j \in [-1, i)\)。转移时枚举点 \(i\),转移到点 \(i\)\(j\),转移到 \(j\) 的点 \(k\)

// 完整代码见末尾,只有 namespace m 的差异
namespace O_n3{ // }{{{
ll dpp[405][405];
ll from[405][405];
auto &dp(int x, int y){ return dpp[x+1][y+1]; }
auto &fr(int x, int y){ return from[x+1][y+1]; }
void work(){
    Interacter::init();
    assert(in <= 400);
    UP(i, 0, in) UP(j, -1, i){
        dp(i, j) = j==-1 ? iasum[i]*iasum[i] : INF;
        ll len = (iasum[i]-iasum[j])*(iasum[i]-iasum[j]);
        UP(k, -1, j){
            if(iasum[j]-iasum[k] <= iasum[i]-iasum[j]) {
                if(dp(j, k)+len < dp(i, j)){
                    dp(i, j) = dp(j, k) + len;
                    fr(i, j) = k;
                }
            }
        }
    }
    ll ans = INF;
    ll k = 0;
    UP(i, -1, in-1){
        if(dp(in-1, i) < ans){
            ans = (dp(in-1, i));
            k = i;
        }
    }
    if(ans/10000000000000000000LLU) cout << (unsigned long long)(ans/10000000000000000000LLU);
    cout << (unsigned long long)(ans%10000000000000000000LLU) << '\n';
}
} // {}}}

\(n \le 4 \times 10^7\) 的数据范围显然不允许我们这么做,需要 \(O(n \log n)\) 的做法。下面提供一个 \(\Theta(n)\) 的做法。

看题解后发现一个结论:在最优划分里,最后一段一定无法缩短。

反证:假设可以缩短,则踢出去的这一部分无论是给别人还是单独成段都比在最后一段优,不满足最优划分。

于是,对于每个点我们只需记录最短一段的长度及费用即可。

发现答案具有单调性,考虑单调队列。

dp 过程中,设正在求答案的点为 \(i\),对于队首的点 \(j\),如果它后面的点 \(k\) 可以接在 \(i\) 前面(即使 \(i\) 结尾的划分最长一段更短),则由之前的结论,\(k\) 一定比 \(j\) 不劣,\(j\) 出队。这样一直到一个不能出队的点 \(l\),它就是可以转移的 \(i\) 的最优答案。

然而,这样还需保证队列中 \(l\) 以后的点全都不能转移到 \(i\)

在加入点时,考虑何时队尾的点会不如当前加入的点:\(back.maxlen - ( \sum_{i \in (last.pos, now.pos]} a_i) \ge now.maxlen\)

也就是 dp.back().lastlen - (iasum[i] - iasum[dp.back().pos]) >= topush.lastlen

不断弹掉这些点即可保证单调性。

每个点至多进队出队一次,总时间是 \(\Theta(n)\) 的。

#include <cassert>
#include <iostream>
#include <list>
#define UP(i,s,e) for(auto i=s; i<e; ++i)
using std::cin; using std::cout;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
constexpr int N = 4e7;
namespace m{ // }{{{
int in; ll iasum_[N+1], *const iasum=iasum_+1;
} // {}}}
namespace Interacter{ // }{{{
using m::in; using m::iasum;
int typ, ix, iy, iz, ib1, ib2, nb, im; // nb = new b
int *ib;
void init(){
    cin >> in >> typ;
    if(typ == 0){
        UP(i, 0, in){
            cin >> iasum[i];
        }
    } else {
        cin >> ix >> iy >> iz >> ib1 >> ib2 >> im;
        ib = new int[in];
        ib[0] = ib1; ib[1] = ib2;
        UP(i, 2, in){
            ib[i] = (
                    + (unsigned)ib[i-2]*(unsigned)iy 
                    + (unsigned)ib[i-1]*(unsigned)ix 
                    + (unsigned)iz
                    ) & 0x3fffffff;
        }
        int ip, il, ir, lastp=0;
        UP(i, 0, im){
            cin >> ip >> il >> ir;
            UP(j, lastp+1, ip+1){
                iasum[j-1] = ib[j-1]%(ir-il+1)+il;
            }
            lastp = ip;
        }
        delete[] ib;
    }
    UP(i, 1, in){
        iasum[i] += iasum[i-1];
    }
}
} // {}}}
namespace m{ // }{{{
extern int in;
extern ll *const iasum;
struct Point{
    int pos; 
    ll lastlen;
    i128 val; // dp val
};
std::list<Point> dp;
void work(){
    Interacter::init();
    dp.push_back({-1, 0, 0});
    UP(i, 0, in){
        auto j = dp.begin();
        while(j != --dp.end()){
            auto k = j; k++;
            if(k->lastlen <= iasum[i]-iasum[k->pos]){
                j = dp.erase(j);
            } else break;
        }
        assert(j->lastlen <= iasum[i]-iasum[j->pos]);
        Point topush = {i, iasum[i]-iasum[j->pos], j->val};
        topush.val += (topush.lastlen) * (i128)(topush.lastlen);
        while(!dp.empty()){
            if(dp.back().lastlen - (iasum[i] - iasum[dp.back().pos]) >= topush.lastlen) dp.pop_back();
            //else if(dp.back().val >= topush.val) dp.pop_back();
            else break;
        }
        dp.push_back(topush);
    }
    i128 ans = dp.back().val;
    // Python 测试了一下 1e19 会掉精度
    if(ans / 10000000000000000000LLU) cout << (ull)(ans/10000000000000000000LLU);
    cout << (ull)(ans%10000000000000000000LLU) << '\n';
}
} // {}}}
int main(){std::ios::sync_with_stdio(0); cin.tie(0); m::work(); return 0;}
posted @   383494  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示