1.14 下午-单调队列 & 斜率优化 & 决策单调性

前言

勿让将来,辜负曾经

好难好难,随机找到了一个师傅收留

正文

知识点

单调队列优化:如果一个选手比你小还比你巨,那么你就可以退役了

斜率优化:在时间线上,一个选手在过去比现在的你菜,在未来比现在的你巨,但是你的能力值低于这位选手的能力值所连成的直线,那么你就也可以退役了

决策单调性优化:如果一个选手在某一刻之后总能发挥出比你巨的实力,那么你就可以退役了

废话结束,正文开始

单调队列优化

首先,单调队列是一种数据结构,最基础的运用就是解决序列连续段的最值问题

多扯一句:虽然单调队列单次操作的时间复杂度可以是 \(O(n)\) 的,但是他的总时间复杂度是一个均摊复杂度(即每个点最多入队、出队一次)

单调队列优化的惯用手段大概是这样的(或许是基本上所有 DP 优化的惯用手段?)

  1. 写个朴素的 DP,时间复杂度不过,考虑优化

  2. 魔改式子

  3. 发现可以使用单调队列优化,完结撒花!

而对于单调队列优化来说,DP 式子魔改之后一般会有如下范式

\[f_i = \max / \min \{ f_j + p_j \} + q_i + c \]

简单说明一下,\(f\) 是 DP 数组,\(p\) 是一个仅和 \(j\) 有关的式子\(q_i\) 是一个仅和 \(i\) 有关的式子,\(c\) 是常数

在这套范式之下,后面的 \(q_i + c\) 可以直接预处理出来,而前面的我们直接用单调队列维护即可(显然是一个连续段最值问题)

P.S. 式子看不懂的话可以把所有的单调队列优化问题视作滑动窗口

经典应用——单调队列优化多重背包

写一下多重背包的转移方程哈,记 \(f_{i,j}\) 表示考虑前 \(i\) 个物品,总重量为 \(j\) 的最大价值

为了方便表示,令 \(w_i\) 表示重量,\(v_i\) 表示价值,\(K_i\) 表示物品 \(i\) 最多选几次

\[f_{i,j} = \max_{0 \le k \le K_i} \{f_{i-1,j-k \times w_i} + k \times v_i\} \]

当然,滚动数组把第一维滚掉,就转化成了

\[f_{j} = \max_{0 \le k \le K} \{f_{j-k \times w} + k \times v\} \]

回想那一套单调队列的范式,我们希望把 \(j\)\(f_{j-k \times w}\) 中剥离出去

我们令 \(j = c \times w + d\),有:

\[f_{j} = \max_{0 \le k \le K} \{ f_{ (c-k) \times w + d} - (c-k) \times v \} + c \times v \]

观察到最值问题中的变量形如 \(c-k\),也就是说,我们只需要对这个余数 \(d\) 做考查即可

安利一篇早年博客

斜率优化

(突然就变成省选难度了,就很离谱)

还是整一个范式比较好——

\[a_i \times b_j + (f_i + p_i) = f_j + q_j \]

看起来其实挺抽象的,但是描述这个式子还是好说的。即对于一个转移方程,当前状态为 \(i\),决策点为 \(j\),要求该转移方程中要同时出现仅与 \(i\) 有关,仅与 \(j\) 有关,同时和 \(i,j\) 有关的项

上面的式子可以写成一个 \(kx + b = y\) 的形式,我们的目的是在 \(k\) 确定的时候,找到一个点 \((x,y)\) 使得 \(b\) 最大(或最小)

结论:这些最优的决策点在平面上会构成一个凸包

凸包的性质:从左往右扫描点集,相邻两点的斜率具有单调性

然后数据结构维护斜率即可

虽然这一讲的例题都可以用单调队列维护斜率,但不是所有的斜率优化都可以使用单调队列。比如决策点的横坐标 \(x\) 不单调的情况,也就是插入点的顺序并不严格按照从左至右的顺序,此时就不可以使用单调队列维护斜率

比较常见的操作是 CDQ 分治或者平衡树(云落不想写)维护

当然,自然会出现决策点所在直线的斜率不单调的情况,不过这种情况依旧可以使用单调队列,只是决策点的选取不能取队首,而应上点科技——wqs 二分

那如果都不单调捏,直接转化为偏序问题,CDQ 就是好用捏!

后面关于单调性的分析有些复杂,还是先从默认横坐标 \(x\) 和斜率 \(k\) 都具有单调性开始学习比较好……

决策单调性优化

安利一篇早年博客,跑路

一题一解

T1 【模板】单调队列(P1886)

链接

板咕,人走

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000050;
int n,m,q1[maxn],q2[maxn],a[maxn];
void getmin(){
    int head=1,tail=0;
    for(int i=1;i<=n;i++){
        while(head<=tail&&q1[head]+m<=i)head++;
        while(head<=tail&&a[i]<a[q1[tail]])tail--;
        q1[++tail]=i;
        if(i>=m)cout<<a[q1[head]]<<" ";
    }
    return;
}
void getmax(){
    int head=1,tail=0;
    for(int i=1;i<=n;i++){
        while(head<=tail&&q2[head]+m<=i)head++;
        while(head<=tail&&a[i]>a[q2[tail]])tail--;
        q2[++tail]=i;
        if(i>=m)cout<<a[q2[head]]<<" ";
    }
    return;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    getmin();
    cout<<endl;
    getmax();
    cout<<endl;
    return 0;
}

T2 宝物筛选(P1776)

链接

单调队列优化多重背包板子……

点击查看代码
#include<iostream>
#define int long long
using namespace std;
const int maxn=1e5+10,maxw=4e4+10;
const int inf=0x3f3f3f3f;
int n,W,v[maxn],w[maxn],m[maxn];
int dp[maxw][2],q[maxw][2];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>W;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i]>>m[i];
	}
    int now=0;
    for(int i=1;i<=n;i++){
    	for(int d=0;d<=w[i]-1;d++){
    		int head=0,tail=-1;
    		for(int k=0;k*w[i]+d<=W;k++){
    			while(head<=tail&&q[head][0]<k-m[i]){
    				head++;
				}
				while(head<=tail&&q[tail][1]<dp[k*w[i]+d][now^1]-k*v[i]){
					tail--;
				}
				tail++;
				q[tail][0]=k;
				q[tail][1]=dp[k*w[i]+d][now^1]-k*v[i];
				dp[k*w[i]+d][now]=q[head][1]+k*v[i];
			}
		}
		now^=1;
	}
    now^=1;
    cout<<dp[W][now]<<endl;
    return 0;
}

T3 PTA-Little Bird(P3572)

链接

很难理解,关于板子题是个黄题,板子题变式是个蓝题这件事

首先这个题目贪心不太可做,因为体力值最小和一次飞跃的距离这两个条件互相牵制,贪心没有什么前途

考虑 DP

我们记 \(f_i\) 表示飞到第 \(i\) 棵树的答案,有转移方程:

\[f_i = \min_{i-k \le j \le i} \{ f_j + [d_j \ge d_i] \} \]

朴素的 DP,朴素的复杂度 \(O(n^2)\)

注意到这个式子的结构并不复杂,并且是求一个连续段的最值问题,单调队列优化可以解决

但是问题出在了艾佛森括号上,这一项是很难拆到 \(\min\) 外面去的,且同时和 \(i,j\) 有关

但我们发现一个性质,相同贡献一定树高更高的更优

也就是维护单调队列的时候,可以将内部决策点维护成严格单调的形式

换言之,即便队首决策点 \(j\) 可能需要花费 \(1\) 的贡献转移到 \(i\) 上,但是由于队列内严格单调,这样的决策转移仍旧是最优的(并列最优也还是最优的!)

所以其实我们并不需要做点什么其他操作,只需要把出队的判定条件加一条即可

实现细节:

  1. 多测清空

  2. 判定条件形如——f[i]<f[j]||(f[i]==f[j]&&d[i]>d[j]),注意逻辑运算符的顺序问题

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int maxn=1e6+5;
int n,T,d[maxn];
int q[maxn],f[maxn];
inline void init(){
    memset(f,0,sizeof(f));
    memset(q,0,sizeof(q));
    return;
}
inline bool check(int i,int j){
    return f[i]<f[j]||(f[i]==f[j]&&d[i]>d[j]);
}
inline void solve(){
    int k;
    cin>>k;
    int hd=1,tl=0;
    q[++tl]=1;
    for(int i=2;i<=n;i++){
        while(hd<=tl&&q[hd]<i-k){
            hd++;
        }
        f[i]=f[q[hd]]+(d[i]>=d[q[hd]]);
        while(hd<=tl&&check(i,q[tl])){
            tl--;
        }
        q[++tl]=i;
    }
    cout<<f[n]<<endl;
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>d[i];
    }
    cin>>T;
    while(T--){
        init();
        solve();
    }
    return 0;
}

T4 WIL(P3594)

链接

P.S. 从这道题目开始,以后云落的代码都要压行了(被可持久化 FHQ 给折磨疯了)

考虑一个朴素的做法,枚举左右端点以及修改区间的位置,时间复杂度 \(O(n^3)\)

然后注意到,当固定右端点的时候,我们总希望左端点尽可能靠左(反之亦然,当固定左端点的时候,尽可能希望右端点靠右),显然双指针,依旧枚举区间位置,时间复杂度 \(O(n^2)\)

发现时间复杂度瓶颈在于枚举区间的位置,事实上,这个操作浪费了很多时间复杂度。结合外面的双指针,注意到内部的修改区间位置枚举也形如一个滑动窗口

我们记 \(s_i = \sum_{k=1}^i w_k\),记 \(t_i=s_i-s_{i-d}\),比较有趣的是,我们相当于在双指针框定的范围内找一个最大的 \(t_i\),这玩意就是单调队列的板子,时间复杂度 \(O(n)\)

代码时间!(压行之后居然只有 \(26\) 行)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e6+5;
int n,p,d,w[maxn],s[maxn],t[maxn],q[maxn],ans;
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>p>>d;
    for(int i=1;i<=n;i++)cin>>w[i];
    for(int i=1;i<=n;i++)s[i]=s[i-1]+w[i];
    for(int i=d;i<=n;i++)t[i]=s[i]-s[i-d];
    int hd=1,tl=0;
    for(int l=1,r=d;r<=n;r++){
        while(hd<=tl&&t[q[tl]]<t[r])tl--;
        q[++tl]=r;
        while(s[r]-s[l-1]-t[q[hd]]>p){
            l++;
            while(hd<=tl&&q[hd]<l+d-1)hd++;
        }
        ans=max(ans,r-l+1);
    }
    cout<<ans<<endl;
    return 0;
}

T5 Mowing the Lawn G(P2627)

链接

典题

比较明显的 DP 问题,记 \(f_{i,0/1}\) 表示考虑到第 \(i\) 头牛,第 \(i\) 头牛 不选/选 的答案

朴素转移如下:

\[f_{i,0} = \max(f_{i-1,0},f_{i-1,1}) \newline f_{i,1} = \max_{i-k \le j \le i-1} \{ f_{j,0} - s_j \} + s_i \]

上面那个直接转移转移完了,下面这个单调队列优化的板子变式

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n,k,a[maxn],s[maxn],f[maxn][2],q[maxn];
inline bool check(int i,int j){return f[i][0]-s[i]>f[j][0]-s[j];}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>k;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
    int hd=1,tl=0;
    q[++tl]=0;
    for(int i=1;i<=n;i++){
        f[i][0]=max(f[i-1][0],f[i-1][1]);
        while(hd<=tl&&q[hd]<i-k)hd++;
        f[i][1]=f[q[hd]][0]-s[q[hd]]+s[i];
        while(hd<=tl&&check(i,q[tl]))tl--;
        q[++tl]=i;
    }
    cout<<max(f[n][0],f[n][1])<<endl;
    return 0;
}

T6 跳房子(P3597)

链接

也是被普及组的题目薄纱了

这道题目和前面的 T4 基本上就差了一个二分答案!

考虑到灵活性越高,就越有能力拿到更多的正权分值,所以答案是具有单调性的,观察数据范围,直接考虑二分答案

现在问题转化成了给定灵活度,求最大的得分

显然可以 DP 做这个子问题,记 \(f_i\) 表示跳到第 \(i\) 格子里,式子不推了,基本上是一样的套路,最后能写成一个这样的形式 \(f_i = \max \{ f_j \} + s_i\),上单调队列维护就行

然后不要忘记灵活度给的限制,要控制决策点的合法性

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5e5+10,inf=9e18;
int n,d,k,x[maxn],s[maxn];
int f[maxn],q[maxn];
inline bool check(int g){
    int L=max(d-g,1ll),R=d+g;
    int hd=1,tl=0;
    f[0]=0;
    for(int l=0,r=1;r<=n;r++){
        while(l<r&&x[r]-x[l]>=L){
            while(hd<=tl&&f[l]>=f[q[tl]])tl--;
            q[++tl]=l++;
        }
        while(hd<=tl&&x[r]-x[q[hd]]>R)hd++;
        if(hd<=tl)f[r]=f[q[hd]]+s[r];
        else f[r]=-inf;
        if(f[r]>=k)return true;
    }
    return false;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>d>>k;
    int mx=0,mn=inf,lst=0;
    for(int i=1;i<=n;i++){
        cin>>x[i]>>s[i];
        if(s[i]>0)mx=max(mx,x[i]-lst),mn=min(mn,x[i]-lst),lst=x[i];
    }
    int l=0,r=max(d-mn,mx-d),ans=-1;
    while(l<=r){
        int mid=l+r>>1;
        if(check(mid))r=mid-1,ans=mid;
        else l=mid+1;
    }
    cout<<ans<<endl;
    return 0;
}

T7 划分(P5665)

链接

这位更是重量级,我嘞个超绝 __int128 或者高精度……

\(O(n^3)\) 的朴素 DP 就不多说了,给个状态式子自己推吧(实际上是云落不想打 LATEX)

\(f_{i,j}\) 表示对前第 \(i\) 个任务分组,第 \(j\) 上一段的结尾编号为 \(j\) 的答案

两个贪心结论

  1. 能多分段就多分段

证明:\((a+b)^2 \ge a^2 + b^2\)

  1. 靠后的段越短越好

证明:\(\forall 0<x^2<y^2,(x+k)^2+y^2<x^2+(k+y)^2\)

引入两个贪心结论后,可以直接解决掉上述朴素 DP 的第二维,时间复杂度 \(O(n^2)\)

具体地,记 \(f_i\) 表示对前 \(i\) 个任务分段的最优解,转移方程式形如 \(f_i = \min \{ f_j + (\sum_{k=j+1}^{i} a_i)^2 \}\)

这玩意长得就很单调队列优化……套个板子就好了哇。求和直接转化成前缀和相减的形式(难度全在卡常上了……)

需要说明的是,这题时空限制很紧,并且需要我们使用 __int128,所以不建议大家写 deque,然后建议使用较快的输入输出方式,以及控制数组类型,不要 #define int __int128

代码实现(云落写的是 __int128 捏)

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=4e7+5,maxm=1e5+5,mod=(1<<30);
int n,type;
ll b[maxn];
int p[maxm],l[maxm],r[maxm];
ll s[maxn];
int pre[maxn],q[maxn];
inline int read(){
	int x=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0')if(c=='-')f=-1,c=getchar();
	while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar();
	return x*f;
}
inline void write(__int128 x){
    if(x<0)putchar('-'),x=-x;
    if(x>9)write(x/10);
    putchar(x%10+'0');
    return;
}
inline __int128 w(int i){return 2*s[i]-s[pre[i]];}
signed main(){
    n=read();
    type=read();
    if(type==0)for(int i=1;i<=n;i++)s[i]=s[i-1]+read();
    else{
        ll x=read(),y=read(),z=read();
        b[1]=read(),b[2]=read();
        ll 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]%mod+y*b[i-2]%mod)%mod+z)%mod;
        int now=0;
        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 hd=1,tl=1;
    for(int i=1;i<=n;i++){
        while(hd<tl&&w(q[hd+1])<=s[i])hd++;
        pre[i]=q[hd];
        while(hd<tl&&w(i)<w(q[tl]))tl--;
        q[++tl]=i;
    }
    int now=n;
    __int128 ans=0;
    while(now){
        __int128 w=s[now]-s[pre[now]];
        ans+=(w*w);
        now=pre[now];
    }
    write(ans),puts("");
    return 0;
}

(摆烂)(感慨)(凑字数)(尖叫)(发癫)

哎,感觉单调队列过的好快啊,这题解写的可真是太水了(甚至 markdown 只码了不到 \(600\) 行,平常这个时候进度过半都 \(1600\) 行力……)

尤其是后面几个题目,其实有注意到云落没有写决策点 \(j\) 的转移范围,倒不是云落忘了,就是单纯地不想敲,好累——

分享一个话题,是云落有感而发(又开始长篇大论埋小彩蛋咯)

Q:怎么玩转 oi
A:……

云落虽然生产鸡汤,但没有给别人喝鸡汤的习惯……毕竟云落也是一个讨厌道德与法治这门学科的人(或许打 oi 的都这样?)

所以说,还是分享个人经历为主好叭!

云落去了西坝之后,六年级下学期开始接触 C++,那时候甚至都不知道五大竞赛这个概念,只是觉得好玩。从自身角度出发,云落觉得这属于一种可以发展的爱好;从家长的角度出发,以后步入大学,走向社会,肯定多多少少要和编程打点交道;从伙伴的角度出发,就是一个词——“从众”

上面都是云落懵懵懂懂学编程的缘起

不知道什么时候知道了 luogu 这个网站,然后看见绿色就很赏心悦目,虽然很多题目云落都不会做,但这不妨碍我对这门学科的热爱

七上,疫情,云落多多少少有点小叛逆,也像大多数人一样,不好好上课。刷视频,玩游戏,写小说……云落可以说是无恶不作,确实也让老师和家长头疼了一阵。云落与这些娱乐项目一直缠绵到了八年级的寒假

文化课成绩肯定是一落千丈呗——oi 呢?

因为每个人的工作效率是有限的,所以为了给那些娱乐项目挤出时间,云落只能在挖空日常学习的时间。可能说的大众话一点,就是玩物丧志。但云落需要完成 oi 的任务,又要快速地完成,于是乎,有个按键就在云落的 oi 生涯中不断放大——“查看题解”

所以在这样的背景下,云落的 oi 基本上算是荒废了

后来八年级下学期,云落接到的通知是有直升压力的(虽然最后的结果是全部直升),于是在外界的压迫下,很被动地和书本打了些交道,键盘也基本上就没怎么碰过了……

暑假,参加了校内组织的集训(或许屏幕前的你们也参加了),玩性丝毫不减,可能是被模拟中考压抑的有点久了,hold 不住地拼了命的玩

八月末,第一次认识教练,然后晕晕乎乎地就跑到了北京中学预备队里,就像小说进入了“承”的部分一样,云落的 oi 生涯进入转折

不是向好的转折,却也是向好的转折……

其实十一的时候听到要停掉文化课集训的时候真的有些兴奋,因为云落又可以肆无忌惮地玩了(的确,当初就是抱着这样的想法去草草地签署停课协议)

开高效进阶,打模拟赛

云落天资确实也不赖,高效进阶的难度也就是中等,很多题目云落就想去偷懒,口胡之后就不想敲代码。教练也给我们发了题解,索性云落就干脆抄抄题解就差不多了

直到模拟赛的分数一次比一次抽象……

当然咯,按照一贯的剧情发展,后面是云落的发愤图强时间,然后就一路走上人生巅峰……事实上不完全是,云落并没有发愤图强,只是没有之前那么摆烂了,而也没有走上人生巅峰,也还顺利地拿到了一个 CSP-S 三等奖的成绩,可喜可贺!

但后面的经历云落不是很想说了,回望刚才那么一大串摸爬滚打的黑暗时期,云落总结了四个问题,这才是云落真正想跟诸位分享的。云落很笃定地说,这四个问题所有人都摆脱不掉,只是有些时候好面子不好意思承认罢了

  1. 目标

为什么一开始云落的工作效率会很低,因为云落根本就没有把 oi 当一回事。在云落当时的认知中,oi 只是一个爱好而已,所以学的很粗糙。或者说,对于那时候的云落来说,oi 对云落的吸引力还不如某二字游戏的某个角色的双爆面板提升的吸引力大。

这一切,都可以归结为——没有清晰的目标

其实,云落并不认为当初错了,因为目标这个东西对于云落来说是个大问题。云落坦然承认,确实,毕竟脑子有些时候比较好使,所以从小到大没有什么特别宏大的目标,或者说没有适配云落的目标

为啥嘞?云落想做,伸手就行了;云落不想做,一脚踹开就好了。而且一路顺风顺水(顺到了八年级上学期),不存在可以难倒自己的问题,不存在惹人心烦的事情,仿佛一切皆是唾手可得

当没有目标给自己的那种强力的约束感的时候,这个事情注定做不好

  1. 面子

承接上文,当云落感受到 oi 的博大精深,发现自己没有办法随意驾驭的时候,又该如何呢?

接下来说的可能很现实,好面子的就慎入吧……

事实上,依照云落的经验,你会去“演戏”,欺骗所有人,包括自己。具体地,当一个题目没有办法让云落做出来的时候,云落会很焦躁

云落可能会害怕,这个题目做不出来会遭到家长的指责(尤其是云落的家庭没有人懂编程,那种对牛弹琴深深的无力感——唉),这个题目做不出来会遭到同学的嘲笑,这个题目做不出来会遭到老师的质疑,这个题目做不出来会被其他竞争对手淘汰,这个题目做不出来会被社会打上“炮灰”“分母”的标签,这个题目做不出来会打击自己那无用的自尊……

所以,当初云落为了逃避,“毅然决然”地踏上了抄袭题解这条路

然后就恶性循环了呗,越菜就越想装,越想装就越要抄题解,越要抄题解就越菜

结果是血淋淋的,模拟赛各种抽象的分数,CSP-S 100 分(虽然可能不是自己的问题,但是运气也是实力的一部分)

某个来自大连的学姐 nyn,云落和他们一块集训过。讨论一道提高组的题目的时候,云落被一道搜索的绿题折磨的死去活来

nyn:这不直接搜搜完了吗?
云落:?????????

很现实,因为屏幕前的你们一定对搜索题目不陌生。这种东西依靠所谓的聪明才智是没有效果的,只能多敲、多练,自然地,云落一点不会写

这种巨大的空缺也体现在模拟赛上,其他人可以有暴力这个选项。而云落就没有,why?因为根本就不知道暴搜怎么敲

于是乎云落只能硬着头皮想正解,然后想不出来,寄。云落还很好面子地说:“唉,不想写暴力,暴力没意义,虽然没想出来正解……”

To be honest,在什么东西都可以好面子,学习这个事上,倒是要反其道而行之——越不要脸越好

如果说,当初真的不为了那点面子,好好夯实基础,云落也不至于代码能力就普及组水平……

现在的云落代码能力也很弱

  1. 效率

这个东西的衡量很有意思,云落分享分享自己的鄙见

都知道有一个公式,叫做:工作效率 \(=\) \(\frac{\text{工作总量}}{\text{工作时间}}\)

时间大家的定义都大差不差,但是工作时间呢?

云落经常在那里敲一会代码就跑去写小说了(现在也这样,而且风采不减当年),那云落写小说的时间就不应该计入

所以,家长有时候会揪着这个点不放:“你在这里一坐坐四五个小时,你就写了这么几个题?你肯定又去打游戏(或刷视频,写小说)了!”

或许听到这里心里会舒坦些,毕竟工作效率没有掉下来。然而……真的没有吗?

举个例子,云落抄题解半个小时就可以刷完一章高效进阶题单(十个题左右),自己写可能只能写一个题

云落能说,前者比后者效率高吗?不能,因为工作总量的定义不是客观的完成了多少题目,而是主观上作为 oi 学习者,工作时间内收获了多少新的或巩固了多少旧的知识

所以嘛……效率这个概念很要命!在这套体系下,云落认为自己的效率很低。就以抄题解这件事为例,有的时候就很很尬,因为那个绿油油的 Accepted 显得很扎眼。毕竟,只需要向自己发问:“为什么 AC 了但自己不会做?”云落表示,好尴尬啊……

但这并不代表抄题解一定要被严令禁止,相反,使用得当反而会有奇效。这学期寒假之后,云落越来越多地接触难度较大的题目,也就是所谓的“紫题”“黑题”。一上午栽进去了毫无收获也是常有的事,这个时候,借鉴他人的思路,不失为一种提升效率的良策

聪明的你会注意到,这个时候我们选择题解,不再是为了“抄题解”,更进一步地说,不是为了那个绿油油的 AC,而是为了真正的学透这道题目,更好地提升自己。说的慷慨激昂一些,曾经是别人的,现在学到了,将来就是自己的

简单类比一下,就像学模板,不一样是“抄袭”模板题的题解么?

  1. 自我

这个概念可能会很大,有些虚无缥缈的感觉。但其实对于云落来说,这个概念却时时刻刻的出现在生活中

停课集训以来,虽然教练有在努力地做旁人的思想工作。但毕竟免不了或多或少的言论传入云落的耳中。家长说:“你这高考怎么办?”老师说:“学业为重”同学说:“你看看他们那些打竞赛的……”

说实话,这些人七嘴八舌的,口水真的可以将云落淹死。可是,我们都忘记了,为什么人生是一场马拉松?人生路漫漫只是一个方面,更重要的是,人和人本来就在不同轨道上。就像马拉松到了中后程,总是有些孤独的,不是么?

云落没有办法分析自己现在做的决定是否是科学理性的,或许在十年后的未来,这个抉择可能是根本错误的。但是,云落坚信,当把自己的精力倾注在一件事上的时候,云落不会后悔

人生高峰的攀登大概是这样的——看过,到过,征服过。或许只有征服过的人会被铭记,芸芸众生都只是停留在看过。或许云落征服不了一座又一座的雪峰,但是,那上面一定有属于自己的脚印

写在废话之后

辩论赛选手的职业病……该圆规正传了

T8 玩具装箱(P3195)

链接

斜率优化的板题

其实 DP 优化的题目一般都是先列出朴素的 DP 转移方程式,然后观察一些比较神奇的性质,套板子优化即可

对于这道题,先列出一个 \(O(n^2)\) 时间复杂度的朴素 DP。具体地,记 \(f_i\) 表示考虑前 \(i\) 个玩具的答案

为了方便表述,记 \(s_i\) 表示 \(i + \sum_{k=1}^{i} c_i\)。转移方程形如:

\[f_i = \min \{ f_j + (s_i - s_j - L - 1)^2 \} \]

括号里的 \(-1\) 好烦,先让 \(L \gets L+1\),变成

\[f_i = \min \{ f_j + (s_i - s_j - L)^2 \} \]

首先式子很好看,其次复杂度不好看,考虑决策单调性斜率优化

P.S. 这道题决策单调性的做法是可以过的!但是众所周知,决策单调性带个 \(\log\)

按照单调队列的套路,我们把括号打开,按照仅和 \(i\) 有关,仅和 \(j\) 有关分类,并且钦定 \(j\) 恰好为决策点,所以脱去 \(\min\),有——

\[2(s_i - L)s_j + f_i - (s_i - L)^2 = f_j + {s_j}^2 \]

你发现就这么个东西没有办法单调队列优化,因为第一项同时与 \(i,j\) 有关,所以可以考虑斜率优化

写成上面这个形式其实也是为了让诸位一眼瞪出斜率优化的范式,显然地,令 \(K=2(s_i-L),X=s_j,Y=f_j+{s_j}^2,B=f_i-(s_i-L)^2\)

然后就写出来 \(KX+B=Y\)

斜率优化是做什么的?让这个截距最大的,并且我们观察到 \(K,X\) 都具有单调性,所以单调队列维护斜率即可

P.S. 各位经常可以看见斜率优化中初始化 hd=1,tl=1 的写法,事实上,其实我们需要插入一个初始点,要不然斜率算着算着就容易出现分母为 \(0\) 的情况

参考代码奉上!

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5e4+5;
int n,L,c[maxn];
int s[maxn],f[maxn],q[maxn],hd,tl;
inline int X(int j){return s[j];}
inline int Y(int j){return f[j]+s[j]*s[j];}
inline double K(int i,int j){return 1.0*(Y(j)-Y(i))/(X(j)-X(i));}
inline void DP(){
    hd=1,tl=1;
    for(int i=1;i<=n;i++){
        while(hd<tl&&K(q[hd],q[hd+1])<=(s[i]-L)*2)hd++;
        int w=s[i]-s[q[hd]]-L;
        f[i]=f[q[hd]]+w*w;
        while(hd<tl&&K(q[tl-1],q[tl])>=K(q[tl-1],i))tl--;
        q[++tl]=i;
    }
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>L;L++;
    for(int i=1;i<=n;i++)cin>>c[i];
    for(int i=1;i<=n;i++)s[i]=s[i-1]+c[i]+1;
    DP();
    cout<<f[n]<<endl;
    return 0;
}

T9 征途(P4072)

链接

不废话,方差等于平方的均值减去均值的平方,乘以长度的平方之后大概就变成了——

长度 \(\times\) 平方和 \(-\) 和的平方

然后随随便便写个 DP,记 \(f_{k,i}\) 表示前 \(k\) 天走 \(i\) 段路,方程不给了

和上一道题目完全一样的套路,列式子,拆括号,移项写成一次函数形式,然后就无了

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=3e4+5;
int n,m,a[maxn];
int s[maxn],sum[maxn];
int f[maxn],q[maxn],hd,tl;
inline int X(int j){return sum[j];}
inline int Y(int j){return s[j]+sum[j]*sum[j];}
inline double K(int i,int j){return 1.0*(Y(j)-Y(i))/(X(j)-X(i));}
inline void DP(){
    for(int k=1;k<m;k++){
        hd=1,tl=0;
        q[++tl]=k;
        for(int i=k+1;i<=n;i++){
            while(hd<tl&&K(q[hd],q[hd+1])<sum[i]*2)hd++;
            int w=(sum[i]-sum[q[hd]]);
            f[i]=s[q[hd]]+w*w;
            while(hd<tl&&K(q[tl-1],q[tl])>K(q[tl-1],i))tl--;
            q[++tl]=i;
        }
        for(int i=1;i<=n;i++)s[i]=f[i];
    }    
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i],s[i]=sum[i]*sum[i];
    DP();
    cout<<m*f[n]-sum[n]*sum[n]<<endl;
    return 0;
}

T10 特别行动队(P3628)

链接

云落拿到这个题目的时候一开始以为自己眼花了,寻思着怎么又来一道玩具装箱

然而,思路完全一致,云落的沉默震耳欲聋(状态设计,转移方程都不想给咋办?)

\(f_i\) 表示前 \(i\) 个的答案,然后式子(甚至还有相同的前缀和……)

\[f_i = \max \{f_j + A(s_i-s_j)^2 + B(s_i-s_j) +C \} \]

云落直接报结论哈!

\[X=s_j \newline Y=f_j + A \times {s_j}^2 - B \times s_j \]

\(K\) 直接求两点所在直线斜率,\(B\) 不需要计算,直接根据上面的式子更新 \(f_i\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+5;
int n,a,b,c,x[maxn];
int s[maxn],f[maxn],q[maxn],hd,tl;
inline int X(int j){return s[j];}
inline int Y(int j){return f[j]+a*(s[j]*s[j])-b*s[j];}
inline double K(int i,int j){return 1.0*(Y(j)-Y(i))/(X(j)-X(i));}
inline void DP(){
    hd=1,tl=1;
    for(int i=1;i<=n;i++){
        while(hd<tl&&K(q[hd],q[hd+1])>=2*a*s[i])hd++;
        int w=s[i]-s[q[hd]];
        f[i]=f[q[hd]]+a*(w*w)+b*w+c;
        while(hd<tl&&K(q[tl-1],q[tl])<=K(q[tl-1],i))tl--;
        q[++tl]=i;
    }
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>a>>b>>c;
    for(int i=1;i<=n;i++)cin>>x[i];
    for(int i=1;i<=n;i++)s[i]=s[i-1]+x[i];
    DP();
    cout<<f[n]<<endl;
    return 0;
}

T11 诗人小 G(P1912)

链接

终于进决策单调性优化 DP 咯!居然是国赛题,稀客稀客

云落就不再重复为什么满足四边形不等式是具有决策单调性的充分条件了哈!去网上或者云落的博客里看看就好了,证明还是挺简单的

还是玩具装箱那道题目,只不过贡献的式子的指数变成了 \(P\),显然拆括号不好做

还是把式子写出来比较好——

\(f_i = \min \{f_j + |s_i - s_j - L - 1|^{P} \}\)

然后把贡献计算的部分拉出来,证明其满足四边形不等式(又是 LATEX 的一天)

嗯哼——相交小于包含,有

\[w(i,j+1)+w(i+1,j) \ge w(i+1,j+1) + w(i,j) \]

简单移项,有

\[w(i+1,j)-w(i+1,j+1) \ge w(i,j) - w(i,j+1) \]

\(w(i,j)\) 的贡献式子自己带进去吧(主要是 LATEX 炸的很频繁,而且云落不想调教这个东东了),一个比较好的技巧是换元法,反正 5min 内肯定能搞定

一顿乱搞之后,转化为证明 \(\forall c\),定义在 \(\mathbb{Z}\) 上的函数 \(f(x) = |x|^p - |x+c|^p\) 单调递减

这东西八仙过海各显神通吧,云落直接选择了最笨的分类讨论+求导,你们嘞?

(其实可以打表观察决策点的单调性)

然后证明了这个方程是有决策单调性的,那么久二分栈或者单调队列或者分治搞一搞就 OK 力

这个题云落写二分栈,下个题写分治,再下个题,诶不对,没有再下个题了……

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define double long double
#define int long long
using namespace std;
const int maxn=1e5+10;
const double inf=1e18,eps=1e-6;
int n,m,k;
string s[maxn];
double sum[maxn],f[maxn];
struct node{int l,r,pos;}q[maxn];
int pre[maxn],S[maxn];
inline double qpow(double a,int b){
    double res=1;
    while(b){
        if(b&1)res*=a;
        a*=a;
        b>>=1;
    }
    return res;
}
inline double cal(int j,int i){
    if(j>i)swap(i,j);
    return f[j]+qpow(fabs(sum[i]-sum[j]+(i-j-1)-m),k);
}
inline void input(){
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++)cin>>s[i];
    return;
}
inline void solve(){
    for(int i=1;i<=n;i++){
        int len=s[i].length();
        sum[i]=sum[i-1]+len;
    }
    int hd=1,tl=0;
    q[++tl]={1,n,0};
    int j=0;
    for(int i=1;i<=n;i++){
        while(hd<tl&&q[hd].r<i)hd++;
        j=q[hd].pos;
        q[hd].l++;
        f[i]=cal(j,i);
        pre[i]=j;
        while(hd<tl&&cal(q[tl].pos,q[tl].l)>=cal(i,q[tl].l))tl--;
        int l=q[tl].l,r=q[tl].r,idx=q[tl].pos,res=r+1;
        while(l<=r){
            int mid=l+r>>1;
            if(cal(i,mid)<=cal(idx,mid))r=mid-1,res=mid;
            else l=mid+1;
        }
        if(res<=n){q[tl].r=res-1;q[++tl]={res,n,i};}
    }
    return;
}
inline void output(){
    if(f[n]-eps>inf){
        cout<<"Too hard to arrange"<<endl;
        cout<<"--------------------"<<endl;
        return;
    }
    cout<<(int)(f[n])<<endl;
    memset(S,0,sizeof(S));
    int now=n,cnt=0;
    while(now)S[++cnt]=now,now=pre[now];
    for(int i=cnt;i>=1;i--){
        for(int j=S[i+1]+1;j<=S[i];j++){
            cout<<s[j];
            if(j!=S[i])cout<<' ';
        }
        cout<<endl;
    }
    cout<<"--------------------"<<endl;
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int T;cin>>T;
    while(T--)input(),solve(),output();
    return 0;
}

T12 Lightning Conductor(P3515)

链接

推一篇云落认为写的很好的文章,也就是 luogu 题解区第一篇

传送门

代码云落放在了一个小角落里,诸位慢慢找哈

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn=5e5+5;
int n,a[maxn];
double dp[maxn],sq[maxn];
inline void init(){for(int i=1;i<=n;i++)sq[i]=sqrt(i);return;}
inline double w(int j,int i){return a[j]*1.0+sq[i-j];}
inline void solve(int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1,pos=0;double mx=0;
	for(int i=L;i<=min(mid,R);i++)
		if(w(i,mid)>mx){
			mx=w(i,mid);
			pos=i;
		}
	dp[mid]=max(dp[mid],mx);
	solve(l,mid-1,L,pos);solve(mid+1,r,pos,R);
	return;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	init();solve(1,n,1,n);
	for(int i=1;i<=n/2;i++)swap(a[i],a[n-i+1]),swap(dp[i],dp[n-i+1]);
	solve(1,n,1,n);
	for(int i=n;i>=1;i--)cout<<(int)ceil(dp[i])-a[i]<<endl;
	return 0;
}

后记

下班下班

完结撒花!

posted @ 2025-03-13 16:31  sunxuhetai  阅读(21)  评论(0)    收藏  举报