跳转至

决策单调性之 单调队列优化

例题 #1 Pictures with Kittens (hard version)

给你一个数列\(a\),你需要选择\(m\)个元素,使得连续的\(k\)个元素都至少有一个被选中。

需要你最大化选出来的所有数的和。

要求\(O(n^2)\)算法


我们首先写出\(O(n^3)\)的代码

定义\(f_{i,j}\)为考虑前i个物品,选j个的最大和,且强制选i。

然后我们就发现\(f_{i,j}\)的注意区间可以使用单调队列维护。每次要转移前,在队列前部弹出已经不在K范围内的。转移后,先弹出队列后部没有当前值优的值,然后再将当前值推入。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define int long long
inline int read(){
    int x;
    cin>>x;
    return x;
}
#define cdbg(x) cerr<<#x<<" : "<<x<<endl;

/*
策略:
间隔不得超过K-1
选择x个的最大价值

f_i,j 考虑前i个,选择了j个且第i个必选的最大价值

*/

const int INF=1e9;
const int N=5e3+5;
int f[N][N];
int a[N];

list<int> ls[N];

signed main(){
    int n=rd,K=rd;
    int x=rd;
    for(int i=1;i<=n;i++){
        a[i]=rd;

    }
    if(n/K>x){
        puts("-1");
        exit(0);
    }

    memset(f,-0x3f3f,sizeof f);
    f[0][0]=0;

    ls[0].push_back(0);

    for(int i=1;i<=n;i++){
        // for(int j=max(0,i-K);j<i;j++){
        for(int k=i;k;k--){
            while(ls[k-1].size()&&ls[k-1].front()<i-K)ls[k-1].pop_front();
            // cerr<<i<<k<<j<<k-1<<endl;
            if(ls[k-1].size()) f[i][k]=max(f[i][k],f[ls[k-1].front()][k-1]+a[i]);
            while(ls[k].size()&&f[ls[k].back()][k]<=f[i][k])ls[k].pop_back();
            ls[k].push_back(i);
        }
        // }
    }


    int ans=-INF;

    for(int i=0;i<K;i++){
        ans=max(ans,f[n-i][x]);
    }

    cout<<ans<<endl;


    return 0;
}

例题 #2 [USACO10NOV] Buying Feed G

题目描述

约翰开车来到镇上,他要带\(K\)吨饲料回家。运送饲料是需要花钱的,如果他的车上有\(X\)吨饲料,每公里就要花费\(X^2\)元,开车D公里就需要\(D\times X^2\)元。约翰可以从\(N\)家商店购买饲料,所有商店都在一个坐标轴上,第\(i\)家店的位置是\(X_i\),饲料的售价为每吨\(C_i\)元,库存为\(F_i\)

约翰从坐标\(X=0\)开始沿坐标轴正方向前进,他家在坐标\(X=E\)上。为了带\(K\)吨饲料回家,约翰最少的花费是多少呢?假设所有商店的库存之和不会少于\(K\)

举个例子,假设有三家商店,情况如下所示:

坐标 \(X=1\) \(X=3\) \(X=4\) \(E=5\)
库存 \(1\) \(1\) \(1\)
售价 \(1\) \(2\) \(2\)

如果\(K=2\),约翰的最优选择是在离家较近的两家商店购买饲料,则花在路上的钱是\(1+4=5\),花在商店的钱是\(2+2=4\),共需要\(9\)元。

输入格式

\(1\)行:三个整数\(K,E,N\)\(2..N+1\)行:第\(i+1\)行的三个整数代表,\(X_i,F_i,C_i\).

输出格式

一个整数,代表最小花费

\(1 \leq K \leq 10000 , 1 \leq E \leq 500 , 1 \leq N \leq 500\)

\(0 < Xi < E, 1 \leq Fi \leq 10000, 1 \leq C_i \leq 10^7\)


思路

理解不难。

Luogu P4544 [USACO10NOV]Buying Feed G - 燃烧的冰块_husky 的博客 - 洛谷博客

std

/*
CB Ntsc
*/

#include <algorithm>
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
#define pii pair<int, int>
#define pf first
#define ps second

#define err cerr<<"Error"
#define rd read()
// #define nl putc('\n')
#define ot write
#define nl putchar('\n')
inline int rd
{
    int xx=0,ff=1;
    char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') ff=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') xx=xx*10+(ch-'0'),ch=getchar();
    return xx*ff;
}
inline void write(int out)
{
    if(out<0) putchar('-'),out=-out;
    if(out>9) write(out/10);
    putchar(out%10+'0');
}

const int INF = 1e13;

const int S=1e6+5;
const int maxlog = 10;

const int N = 510;
const int M = 10010;

template <class T>
inline T min(T a, T b) {
    return a < b ? a : b;
}

struct node {
    int x;
    int w;
    int v;

    node(): x(0), w(0), v(0){}
    node(int x_, int w_, int v_): x(x_), w(w_), v(v_) {}
    inline bool operator < (node &a) {
        return this->x < a.x;
    }
};

node s[N];
int W, n, d[N][M];
int  que[M];

inline int calc(int i, int k) {
    return d[i - 1][k] + (s[i].x - s[i - 1].x) * k * k - s[i].v * k;
}

signed main() {
    int E;
    W=rd,E=rd,n=rd;
    s[0] = node(0, 0, 0);   
    for (int i = 1, x, w, v; i <= n; ++i) {
        x=rd,w=rd,v=rd;
        s[i] = node(x, w, v);
    }
    s[n + 1] = node(E, 0, 0);  
    sort(s, s + n + 2);
    memset(d, 0x3f, sizeof(d));
    d[0][0] = 0;
    for (int i = 1; i <= n + 1; ++i) {   
        int l = 1, r = 0;
        for (int j = 0; j <= W; ++j) {   
            while (calc(i, que[r]) > calc(i, j) && l <= r)
                --r;
            if (j - que[l] > s[i].w && l <= r)
                ++l;
            que[++r] = j;
            d[i][j] = calc(i, que[l]) + s[i].v * j;           
        }
    }
    printf("%lld", d[n + 1][W]);
}

/*
2 5
0 1 1 1 1
0 1 1 2 4
0 2 1 2 1
0 2 1 1 4
*/

例题 #3 [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\),请你求出最优划分方案下,小明的程序的最小运行时间。

输入格式

由于本题的数据范围较大,部分测试点的 \(a_i\) 将在程序内生成。

第一行两个整数 \(n, type\)\(n\) 的意义见题目描述,\(type\) 表示输入方式。

  1. \(type = 0\),则该测试点的 \(a_i\) 直接给出。输入文件接下来:第二行 \(n\) 个以空格分隔的整数 \(a_i\),表示每组数据的规模。

  2. \(type = 1\),则该测试点的 \(a_i\) 将**特殊生成**,生成方式见后文。输入文件接下来:第二行六个以空格分隔的整数 \(x, y, z, b_1, b_2, m\)。接下来 \(m\) 行中,第 \(i (1 \leq i \leq m)\) 行包含三个以空格分隔的正整数 \(p_i, l_i, r_i\)

对于 \(type = 1\) 的 23~25 号测试点,\(a_i\) 的生成方式如下:

给定整数 \(x, y, z, b_1, b_2, m\),以及 \(m\) 个三元组 \((p_i, l_i, r_i)\)

保证 \(n \geq 2\)。若 \(n \gt 2\),则 \(\forall 3 \leq i \leq n, b_i = (x \times b_{i−1} + y \times b_{i−2} + z) \mod 2^{30}\)

保证 \(1 \leq p_i \leq n, p_m = n\)。令 \(p_0 = 0\),则 \(p_i\) 还满足 \(\forall 0 \leq i \lt m\)\(p_i \lt p_{i+1}\)

对于所有 \(1 \leq j \leq m\),若下标值 \(i (1 \leq i \leq n)\)满足 \(p_{j−1} \lt i \leq p_j\),则有

\(a_i = \left(b_i \mod \left( r_j − l_j + 1 \right) \right) + l_j\)

上述数据生成方式仅是为了减少输入量大小,标准算法不依赖于该生成方式。

输出格式

输出一行一个整数,表示答案。

【数据范围】

测试点编号 \(n \leq\) \(a_i \leq\) \(type =\)
\(1 \sim 3\) \(10\) \(10\) 0
\(4 \sim 6\) \(50\) \(10^3\) 0
\(7 \sim 9\) \(400\) \(10^4\) 0
\(10 \sim 16\) \(5000\) \(10^5\) 0
\(17 \sim 22\) \(5 \times 10^5\) \(10^6\) 0
\(23 \sim 25\) \(4 \times 10^7\) \(10^9\) 1

对于\(type=0\)的所有测试点,保证最后输出的答案\(\leq 4 \times 10^{18}\)

所有测试点满足:\(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}\)

思路

可以证明在条件允许下分得越多时间越小。在划分数目相同的情况下,我们尽可能让每一块平均。预计时间复杂度为\(O(n)\)

\(f(i,j)\):当前已经划分好了前\(i\)个数,下一次直接划分出\([i+1,j]\)的最小运行时间。

设前缀和\(s_i=\sum_{j=1}^ia_j\),然后有转移:\(f(i,j)=\min_{(0\le k<i)\land (s_i-s_k\le s_j-s_i)}\{f(k,i)+(s_j-s_i)^2\}\) ~~不难发现,转移的时候,如果\(i\)相同,那么随着\(j\)的增大,\(k\)的取值范围是增大的。所以从小到大枚举\(j\),然后挪动\(k\),用一个变量维护\(\min\{f(k,i)\}\),计算得到答案。

我们不难发现如下的性质: $a2+b2\le(a+b)^2 $这个式子告诉了我们尽量多分段。

既然如此,对于每一个\(i\),转移的时候,我们实际上只需要找到第一个可行的\(k\)就可以了(有一些\(k\)会导致无法成功分段)。用\(g(i)\)来记录这个第一个\(k\)

考虑快速求出这个\(g(i)\)。从\(g(i)\)的定义入手: **\(g(i)\)是最大的\(k\)使得\(s_i-s_k\ge s_k-s_{g(k)} (**(即满足递增条件)移项的到: \(s_i\ge 2s_k-s_{g(k)}\) 同时,\)g(i)\)是递增的。所以对于\(i\),如果\(j<g(i)\),那么\(j\)不可能成为\(g(i+1)\),这就是决策的单调性,因此我们从前往后扫一遍即可。

另外,由于\(\forall 1\le i\le n,1\le g(i)< i\),所以我们可以用一个单调队列来维护可能可以成为答案的\(j\),然后每次找到单调队列里面最大的且可行的那个作为\(g(i)\)即可。 考虑往单调队列里面插入一个值。设\(v(i)=2s_i-s_{g(i)}\),则我们应该在插入的时候弹掉队尾的所有\(v\ge v(i)\)的值。假如\(v\)对应的位置可以成为一个\(g\),那么\(i\)必然可以,而且更优。

代码

卡空间

#include<bits/stdc++.h>
#define rep(l,r,i) for(int i=l,END##i=r;i<=END##i;++i)
#define per(r,l,i) for(int i=r,END##i=l;i>=END##i;--i)
using namespace std;
#define int long long
#define pii pair<int,int>
#define LL __int128

#define lc(x) (x<<1)
#define rc(x) (x<<1|1)

#define rd read()
int read(){
    int xx=0,ff=1;
    char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') ff=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') xx=xx*10+(ch-'0'),ch=getchar();
    return xx*ff;
}
void write(LL out){
    if(out<0) putchar('-'),out=-out;
    if(out>9) write(out/10);
    putchar(out%10+'0');
}

const int N=2e5+15;
const int INF=1e9+5;
const int MOD = 1<<30;


const int MAXN = 40000005;
const int MAXM = 100005;


signed n,type,q[MAXN],g[MAXN];
int l[MAXM],r[MAXM],p[MAXM];
int s[MAXN],b[MAXN];
int calc(int x) {return 2*s[x]-s[g[x]];}
signed main()
{
    n=read();type=read();
    if(type==0)
    {
    // cerr<<"YES";
        for(int i=1;i<=n;i++)
            s[i]=s[i-1]+read();
    }
    else
    {
        int x=read(),y=read(),z=read(),m,now=1;
        b[1]=read();b[2]=read();m=read();
        for(int i=1;i<=m;i++)
            p[i]=read(),l[i]=read(),r[i]=read();
        for(int i=3;i<=n;i++)
            b[i]=(x*b[i-1]+y*b[i-2]+z)%MOD;
        for(int i=1;i<=n;i++)
        {
            if(i>p[now]) now++;
            s[i]=s[i-1]+(b[i]%(r[now]-l[now]+1))+l[now];
        }
    }
    int head=1,tail=1;

    for(int i=1;i<=n;i++)
    {
        while(head<tail && calc(q[head+1])<=s[i]) head++;
        g[i]=q[head];
        while(head<tail && calc(q[tail])>=calc(i)) tail--;
        q[++tail]=i;
    }
    int now=n;LL ans=0;
    while(now) ans+=((LL)s[now]-s[g[now]])*(s[now]-s[g[now]]),now=g[now];
    write(ans);
}