一条直线上有N个村庄,要在这条直线上选K个地方建雕像,使得每个村庄到离其最近的雕像的距离的和最小。输出最小的和。(范围1e5)
首先考虑一堆村庄建一个雕像,则最优方案一定是放在中间的村庄(偶数的话中间两个都可以)
那么考虑最优方案一定被包含于,所有的把整个序列划分成k段,并在每段中挑选中间位置建雕像的方案。
可以dp,dp到某个位置i建了j个雕像的最小代价。
转移dp(i,j) = min { dp(x - 1,j - 1)+ cost(x,i)}
cost是i,j这一段在中间建雕像的代价,可O(1)查询;该转移方程表示当前位置为右端点,枚举所有该段左端点的选取。
发现复杂度n^3...
又可以发现,这个东西满足wqs二分性(凸性)。简单点说,就是假设当前这n个村庄,让你建k个雕像的代价是res(k),则有res(k + 1)- res(k) >= res(k + 2) - res(k + 1).
也就是说,让你多建一个雕像,能让答案变小的贡献其实是单调不增的。
(不严谨的)证明应该很简单,一种建立k个雕像的方案,考虑把k个雕像依次建立,如果后面建立的雕像使代价的减小量能多于之前建立的雕像,则显然可以调换它,让它在之前建立得到大于等于现在减少代价量的贡献,故建立雕像时一定可以做到上述凸性。
这种性质的问题,可以二分一个额外贡献val,在选取一个雕像时要额外花费val的代价,则由凸性,val的增加会导致选的雕像会越来越少,而对于一个val,转移方程变成了:
dp(i) = min { dp(x - 1)+ cost(x, i))+ val},同时维护选的雕像数(作为总代价的第二关键字)
根据此时最优方案选择的雕像数量调整二分值,直到二分到一个val,此时最优方案选择的最小雕像数小于等于k(而val+1对应的就大于k),则以val为额外代价dp,dp出的答案减去 val* k就是实际答案。(最小雕像数可能不是恰好k,是因为一个val值对应的最优方案选取的雕像数其实是一段区间)
这样就用logn的时间减少了一维dp,复杂度优化到了n^2logn
继续考虑确定了一个val下的dp如何优化:
发现当i递增时,dp(i)的最优决策点(多个,选最靠后的)是单调不减的,但不一定是连续的增长,故不能直接暴力右移决策位置.
考虑维护一个存放决策点队列,里面存的是决策点位置,以及该位置为决策点管辖的区间。
也就是说,队列里一个元素记为(ps,l,r),表示根据当前已知的所有dp值,之后访问到的(l,r)区间内元素决策位置都会是ps。
显然根据决策点随i递增而单调不减的性质,这个队列实际上是单调队列,即队列中元素ps递增。
那么考虑枚举到i,要对队列如何操作:
1.弹队首,直到队首元素的管辖区间包含i,选取队首元素的ps作为i的决策点即可算出dp(i)。
2.把i作为决策点插入队列。因为最优决策点单调不减性,则对于队尾元素,如果对于队尾元素管辖区间的左端点,选取i都比选取队尾元素的ps优,则直接弹掉队尾元素。重复弹队尾元素,直到:
1.队列为空,直接扔进i决策点,管辖后面所有位置即可。
2.剩下的队尾元素,满足在队尾元素管辖的左端点还是选队尾元素更优,则二分队尾元素管辖的l,r区间,找到队尾元素管辖范围与i管辖范围的分界点,然后修改队尾元素右端点,并插入决策i。
此时对于一个val的dp复杂度就是nlogn了
故总复杂度nlog^2n
#include<bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define sc second
#define pb push_back
#define ll long long
#define trav(v,x) for(auto v:x)
using namespace std;
const int N = 3e5 + 100;
int n, k, T;
int a[N];
ll sum[N];
struct node{
int lp, rp, x;
node(){};
node(int lp, int rp, int x):lp(lp), rp(rp), x(x){};
};
ll dp[N], f[N];
deque<node> q;
int sol(ll val)
{
auto w = [&](int l, int r)
{
int mid = l + r >> 1;
return 1LL * a[mid] * (mid + mid - l - r + 1) + (sum[r] + sum[l - 1] - sum[mid] - sum[mid]);
};
auto calc = [&](int x, int i)
{
return dp[x] + w(x + 1, i) + val;
};
auto cmp = [&](int x1, int x2, int i)
{
ll w1 = calc(x1, i), w2 = calc(x2, i);
if(w1 < w2) return 1;
if(w1 == w2 && f[x1] < f[x2]) return 1;
return 0;
};
auto find = [&](int x1, int x2, int l, int r)
{
int mid, res = r + 1;
while(l <= r)
{
mid = l + r >> 1;
if(cmp(x1, x2, mid)){
res = mid;
r = mid - 1;
}
else l = mid + 1;
}
return res;
};
while(!q.empty())q.pop_back();
dp[0] = 0, f[0] = 0;
q.push_back(node(1, n, 0));
node nw;
for(int i = 1, x, ps; i <= n; i++)
{
while(!q.empty() && q.front().rp < i)q.pop_front();
x = q.front().x;
dp[i] = calc(x, i);
f[i] = f[x] + 1;
while(!q.empty() && cmp(i, q.back().x, q.back().lp))q.pop_back();
if(q.empty()){
q.push_back(node(i + 1, n, i));
}
else{
ps = find(i, q.back().x, q.back().lp, q.back().rp);
q.back().rp = ps - 1;
if(ps <= n)q.push_back(node(ps, n, i));
}
}
return f[n];
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; i++)
cin >> a[i], sum[i] = sum[i - 1] + a[i];
ll l = 0, r = sum[n], mid, res, ans;
int num;
while(l <= r)
{
mid = l + r >> 1;
num = sol(mid);
if(num <= k) res = mid, r = mid - 1;
else l = mid + 1;
}
sol(res);
ans = dp[n] - res * k;
cout << ans << '\n';
return 0;
}