2010-1100
从 Trac 迁移的文章
这是从旧校内 Wiki 迁移的文章,可能存在一些样式问题,您可以向 memset0 反馈。
原文章内容如下:
设dp[n][k]表示将前n个小朋友分到前k个班中,易写出状态转移方程:
dp[n][k] = min {dp[i][k - 1] + gk * (sum[n] - sum[i]) | A <= n - i <= B}
sum[n] = sigma {(xi - L)^2^ | i <= n}
但现在的状态数为10000 × 200,转移复杂度为O(n),显然会TLE
将转移方程整理一下:
dp[n][k] = gk * sum[n] + min {dp[i][k - 1] - gk * sum[i] | n - B <= i <= n - A}
固定k时,当n增大,方程的形式不变,只是可选范围改变
对于n转移到n+1时,i=n-B的状态不可再选,而i=n-A的状态应该加入可选范围内
故可维护一个单调递增队列,每次转移之前将新的可选状态从队列尾插入,且把比其大的都pop出,同时保证队列头的元素在可选范围之内。
这样每次转移就可以只取队列头的元素,将转移复杂度降为O(1)。
总的时间复杂度为O(n*k),空间复杂度为O(n)。
{{{
#!cpp
#include <cstdio>
#include <cstring>
#include <cassert>
#include <algorithm>
using namespace std;
typedef long long LL;
typedef pair<LL, int> PII;
const LL INF = 223372036854775807LL;
const int MAXN = 10010;
const int MAXK = 210;
class Queue {
public:
PII v[MAXN];
int open, closed;
void init() {
open = closed = 0;
}
bool empty() {
return closed <= open;
}
void push(PII now) {
v[closed++] = now;
}
void pop_back() {
closed--;
}
void pop_front() {
open++;
}
PII back() {
return v[closed - 1];
}
PII front() {
return v[open];
}
}Q;
LL f[MAXN][2];
LL s[MAXN], s2[MAXN], P[MAXN], x[MAXN], g[MAXK];
inline void insert(int t, int pos, int k) {
if (f[pos][t] == INF) return;
LL fv = f[pos][t] - g[k] * P[pos];
while (!Q.empty() && Q.back().first >= fv) {
Q.pop_back();
}
Q.push(make_pair(fv, pos));
}
inline LL get(int atleast, int &ret) {
while (!Q.empty() && Q.front().second < atleast) {
Q.pop_front();
}
ret = -1;
if (Q.empty()) return INF;
ret = Q.front().second;
return Q.front().first;
}
int main() {
int N, K, A, B;
while (scanf("%d %d %d %d", &N, &K, &A, &B) != EOF) {
assert(1 <= N && N <= 10000);
assert(1 <= K && K <= 200);
assert(1 <= A && A <= B && B <= N);
LL L = 0;
for (int i = 1; i <= N; i++) {
scanf("%lld", &x[i]);
assert(1 <= x[i] && x[i] <= 100000);
L += x[i];
}
L /= N;
P[0] = 0;
for (int i = 1; i <= N; i++) {
P[i] = P[i - 1] + (x[i] - L) * (x[i] - L);
}
for (int i = 1; i <= K; i++) {
scanf("%lld", &g[i]);
assert(-1000 <= g[i] && g[i] <= 1000);
}
int t0 = 0, t1;
// k = 0
f[0][t0] = 0LL;
for (int i = 1; i <= N; i++) {
f[i][t0] = INF;
}
LL ans = INF;
int ansk, ansopt, opt;
for (int k = 1; k <= K; k++) {
t1 = 1 - t0;
Q.init();
for (int n = 0; n <= N; n++) {
if (n < k * A || n > k * B) {
f[n][t1] = INF;
continue;
}
insert(t0, n - A, k);
f[n][t1] = g[k] * P[n] + get(n - B, opt);
if (opt == -1) f[n][t1] = INF;
if (n == N) {
if (f[n][t1] < ans) {
ans = f[n][t1];
ansk = k;
ansopt = N - opt;
}
}
}
t0 = t1;
}
if (ans == INF) {
puts("No solution.");
}
else {
printf("%lld %d %d\n", ans, ansk, ansopt);
}
}
return 0;
}
}}}
设dp[n][k]表示将前n个小朋友分到前k个班中,易写出状态转移方程:
dp[n][k] = min {dp[i][k - 1] + gk * (sum[n] - sum[i]) | A <= n - i <= B}
sum[n] = sigma {(xi - L)2 | i <= n}
但现在的状态数为10000 × 200,转移复杂度为O(n),显然会TLE
将转移方程整理一下:
dp[n][k] = gk * sum[n] + min {dp[i][k - 1] - gk * sum[i] | n - B <= i <= n - A}
固定k时,当n增大,方程的形式不变,只是可选范围改变
对于n转移到n+1时,i=n-B的状态不可再选,而i=n-A的状态应该加入可选范围内
故可维护一个单调递增队列,每次转移之前将新的可选状态从队列尾插入,且把比其大的都pop出,同时保证队列头的元素在可选范围之内。
这样每次转移就可以只取队列头的元素,将转移复杂度降为O(1)。
总的时间复杂度为O(n*k),空间复杂度为O(n)。
#include <cstdio>
#include <cstring>
#include <cassert>
#include <algorithm>
using namespace std;
typedef long long LL;
typedef pair<LL, int> PII;
const LL INF = 223372036854775807LL;
const int MAXN = 10010;
const int MAXK = 210;
class Queue {
public:
PII v[MAXN];
int open, closed;
void init() {
open = closed = 0;
}
bool empty() {
return closed <= open;
}
void push(PII now) {
v[closed++] = now;
}
void pop_back() {
closed--;
}
void pop_front() {
open++;
}
PII back() {
return v[closed - 1];
}
PII front() {
return v[open];
}
}Q;
LL f[MAXN][2];
LL s[MAXN], s2[MAXN], P[MAXN], x[MAXN], g[MAXK];
inline void insert(int t, int pos, int k) {
if (f[pos][t] == INF) return;
LL fv = f[pos][t] - g[k] * P[pos];
while (!Q.empty() && Q.back().first >= fv) {
Q.pop_back();
}
Q.push(make_pair(fv, pos));
}
inline LL get(int atleast, int &ret) {
while (!Q.empty() && Q.front().second < atleast) {
Q.pop_front();
}
ret = -1;
if (Q.empty()) return INF;
ret = Q.front().second;
return Q.front().first;
}
int main() {
int N, K, A, B;
while (scanf("%d %d %d %d", &N, &K, &A, &B) != EOF) {
assert(1 <= N && N <= 10000);
assert(1 <= K && K <= 200);
assert(1 <= A && A <= B && B <= N);
LL L = 0;
for (int i = 1; i <= N; i++) {
scanf("%lld", &x[i]);
assert(1 <= x[i] && x[i] <= 100000);
L += x[i];
}
L /= N;
P[0] = 0;
for (int i = 1; i <= N; i++) {
P[i] = P[i - 1] + (x[i] - L) * (x[i] - L);
}
for (int i = 1; i <= K; i++) {
scanf("%lld", &g[i]);
assert(-1000 <= g[i] && g[i] <= 1000);
}
int t0 = 0, t1;
// k = 0
f[0][t0] = 0LL;
for (int i = 1; i <= N; i++) {
f[i][t0] = INF;
}
LL ans = INF;
int ansk, ansopt, opt;
for (int k = 1; k <= K; k++) {
t1 = 1 - t0;
Q.init();
for (int n = 0; n <= N; n++) {
if (n < k * A || n > k * B) {
f[n][t1] = INF;
continue;
}
insert(t0, n - A, k);
f[n][t1] = g[k] * P[n] + get(n - B, opt);
if (opt == -1) f[n][t1] = INF;
if (n == N) {
if (f[n][t1] < ans) {
ans = f[n][t1];
ansk = k;
ansopt = N - opt;
}
}
}
t0 = t1;
}
if (ans == INF) {
puts("No solution.");
}
else {
printf("%lld %d %d\n", ans, ansk, ansopt);
}
}
return 0;
}