[斜率优化] 特别行动队 commando

特别行动队
【问题描述】
你有一支由 n 名预备役士兵组成的部队,士兵从 1 到 n 编号,要将他们拆分成若干特别行动队调入战场。出于默契的考虑,同一支特别行动队中队员的编号应该连续,即为形如(i, i + 1, …, i + k)的序列。
编号为 i 的士兵的初始战斗力为 xi ,一支特别行动队的初始战斗力 x 为队内士兵初始战斗力之和,即 x = xi + xi+1 + … + xi+k。
通过长期的观察,你总结出一支特别行动队的初始战斗力 x 将按如下经验公式修正为 x':x' = ax2 + bx + c,其中 a, b, c 是已知的系数(a < 0)。
作为部队统帅,现在你要为这支部队进行编队,使得所有特别行动队修正后战斗力之和最大。试求出这个最大和。
例如,你有 4 名士兵,x1 = 2, x2 = 2, x3 = 3, x4 = 4。经验公式中的参数为 a = –1,b = 10, c = –20。此时,最佳方案是将士兵组成 3 个特别行动队:第一队包含士兵
1 和士兵 2,第二队包含士兵 3,第三队包含士兵 4。特别行动队的初始战斗力分别为 4, 3, 4,修正后的战斗力分别为 4, 1, 4。修正后的战斗力和为 9,没有其它
方案能使修正后的战斗力和更大。
【输入格式】
输入由三行组成。第一行包含一个整数 n,表示士兵的总数。第二行包含三个整数 a, b, c,经验公式中各项的系数。第三行包含 n 个用空格分隔的整数 x1,x2, …, xn,分别表示编号为 1, 2, …, n 的士兵的初始战斗力。
【输出格式】
输出一个整数,表示所有特别行动队修正后战斗力之和的最大值。
【样例输入】
4
-1 10 -20
2 2 3 4
【样例输出】
9
【数据范围】
20%的数据中,n ≤ 1000;
50%的数据中,n ≤ 10,000;
100%的数据中,1 ≤ n ≤ 1,000,000,–5 ≤ a ≤ –1,|b| ≤ 10,000,000,|c| ≤10,000,000,1 ≤ xi ≤ 100。

之所以用这道题练手是因为印象中其方程特别裸:f[i] = max{f[j] + a * sum(j + 1, i) ^ 2 + b * sum(j + 1, i) + c}。

如何用所谓的斜率优化来写这道题呢?这不像二分栈一样,一看到就可以直接写,反正不会错,而是需要证明并求出斜率才可以。

如何求斜率呢?我套用一般资料上所说的方法:设决策 j > k 且 g[k] > g[j] (g 为转移的代价),则可以展开代价得到一个类似于斜率的不等式:

(yk - yj) / (xk - xj) < h[i] (y 和 x 分别为随决策变化的表达式,而h[i] 为与 k, j 无关而仅与转出有关的常数,总之就是移项然后除一下)。

如此只要不等式成立且 h[i] 随 i 单调递增,则 k 决策将永远优于 j 决策(因为这里所用的是小于号,如果当前小于那么就会一直小于)。

如何利用这个性质呢?可以用单调队列维护决策和斜率单调,并且每次剔除无用决策。同于一般的单调队列优化,每个元素出队入队最多一次,所以时间复杂度为 O(n) 。

至于编程复杂度,完虐二分栈。。。。而且常数和时间复杂度也都优很多。。。试想 100w 或 200w 的数据而时限 1s,二分栈可以跑到一两秒,而斜率优化无压力。。。

Code : 

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>

#define swap(a, b, t) ({t _ = (a); (a) = (b); (b) = _;})
#define max(a, b) ({int _ = (a), __ = (b); _ > __ ? _ : __;})
#define min(a, b) ({int _ = (a), __ = (b); _ < __ ? _ : __;})

#define maxn 1000005

#define sqr(a) ({double _ = (a); _ * _;})

int n, h, t;
int q[maxn];
double a, b, c;
double x[maxn], s[maxn], f[maxn];

double slope(int i, int j)
{
    return (f[j] - f[i] + a * (sqr(s[j]) - sqr(s[i])) + b * (s[i] - s[j])) / (s[j] - s[i]);
}

int main()
{
    freopen("commando.in", "r", stdin);
    freopen("commando.out", "w", stdout);
    
    scanf("%d%lf%lf%lf", & n, & a, & b, & c);
    for (int i = 1; i <= n; ++ i)
        scanf("%lf", & x[i]), s[i] = s[i - 1] + x[i];
    q[++ t] = 0;
    for (int i = 1; i <= n; ++ i)
    {
        while (h + 1 < t && slope(q[h + 2], q[h + 1]) > 2 * a * s[i]) ++ h;
        f[i] = f[q[h + 1]] + a * sqr(s[i] - s[q[h + 1]]) + b * (s[i] - s[q[h + 1]]) + c;
        while (t - 1 > h && slope(q[t - 1], q[t]) < slope(q[t], i)) -- t; q[++ t] = i;
    }
    printf("%.0lf\n", f[n]);
    
    return 0;
}

再是二分栈的代码,瞬间长了一倍(包括头文件等),而且抛开读入慢得多:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <climits>
#include <iostream>
#include <algorithm>

typedef double TYPE;
typedef long long int64;
typedef unsigned int uint;
typedef unsigned long long uint64;

#define swap(a, b, t) ({t _ = (a); (a) = (b); (b) = _;})
#define MAX(a, b, t) ({t _ = (a), __ = (b); _ > __ ? _ : __;})
#define MIN(a, b, t) ({t _ = (a), __ = (b); _ < __ ? _ : __;})
#define max(a, b) ({TYPE _ = (a), __ = (b); _ > __ ? _ : __;})
#define min(a, b) ({TYPE _ = (a), __ = (b); _ < __ ? _ : __;})

#define maxn 1000005
#define oo 0xFFFFFFF
#define sum(i, j) (s[j] - s[(i) - 1])
#define xxx(k) ({double _ = k; A * _ * _ + B * _ + C;})

double A, B, C;
int n, head, tail;
double a[maxn], s[maxn], f[maxn];
struct que{int l, r, x;} q[maxn];

void push(int l, int r, int x)
{
     q[++ tail].x = x;
     q[tail].l = l, q[tail].r = r;
}

int pop_head()
{
     int ans = q[head + 1].x;
     if (++ q[head + 1].l > q[head + 1].r) ++ head;
     return ans;
}

void pop_tail()
{
     if (! -- tail) return;
     q[tail].r = q[tail + 1].r;
}

int main()
{
     freopen("commando.in", "r", stdin);
     freopen("commando.out", "w", stdout);
     
     scanf("%d%lf%lf%lf", & n, & A, & B, & C);
     for (int i = 1; i <= n; ++ i)
          scanf("%lf", & a[i]), s[i] = s[i - 1] + a[i], f[i] = - oo;
     push(1, n, 0);
     for (int i = 1; i <= n; ++ i)
     {
          int j = pop_head();
          f[i] = f[j] + xxx(sum(j + 1, i));
          while (head < tail)
          {
               int l = q[tail].l, r = q[tail].r, x = q[tail].x;
               if (f[i] + xxx(sum(i + 1, r)) < f[x] + xxx(sum(x + 1, r))) break;
               if (f[i] + xxx(sum(i + 1, l)) > f[x] + xxx(sum(x + 1, l))) {pop_tail(); continue;}
               for (int m = l + r >> 1; l < r; m = l + r >> 1)
                    f[i] + xxx(sum(i + 1, m)) > f[x] + xxx(sum(x + 1, m)) ? r = m : l = m + 1;
               q[tail].r = l - 1, push(l, n, i); break;
          }
          if (head == tail) push(i + 1, n, i);
     }
     printf("%.0lf\n", f[n]);
     
     return 0;
}






  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值