题目描述
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;}
标签:小明,P5665,lastlen,back,iasum,leq,S2019,CSP,dp
From: https://www.cnblogs.com/x383494/p/17674936.html