线段树优化dp题单&题解
Tips:右边有目录
前言
前置知识:线段树,dp
线段树优化dp是什么呢?
把 \(O(n^2)\) 的 dp 用线段树优化到 \(O(n\log n)\)
一般做题步骤:
-
想如何暴力 dp
-
观察转移方程,如何用线段树优化
好了,你已经会了,让我们看题吧。(题目简介自己看原题)
CF115E Linear Kingdom Races
首先想贪心,发现不太行,考虑用 dp 做。
设 \(f_i\) 为前 \(i\) 场比赛中的答案,发现需要对区间排序,然而会有包含的情况,无法转移。
那就设 \(f_i\) 为前 \(i\) 条道路中的答案。
设目前的比赛为 \([l,r]\),那么我们需要知道这中间的道路有没有被修。
于是设 \(f[i][j]\) 为第 \(i\) 条道路前,从第 \(j\) 条道路开始往后都要修的答案(不修 \(j\))
怎么算 \(f[i][j]\) 呢?它能被 \(f[i-1][j]\) 转移得到,然后又能被所有符合条件的比赛区间 \([l_i,r_i]\) 加上 \(w_i\) 的贡献。
什么是符合条件的呢?当且仅当 \(j<l_i,r_i=i\),因为这些比赛的道路都被修好了,是可行的,并且不能与之前算重。
所以将比赛区间按右端点排序,枚举 \(i\),再枚举以 \(i\) 为右端点的区间,它就能给 \(f[i][0\sim l_i-1]\) 加上 \(w_i\)。
最后 \(f[i][i]\) 就是目前的答案,然后更新全局答案 \(ans\)(注意与 \(0\) 取 \(\max\))。
多完美啊,直到你看到 \(n\) 的范围是 \(2\times 10^5\)
这东西怎么用线段树维护呢?
枚举 \(i\),将线段树上的第 \(j\) 个位置看成 \(f[i][j]\)
当我将 \(i\) 往右移时,先将所有的 \(f[i][j]\) 都会减去 \(a_i\)(修路的代价)
然后还是将比赛区间排序,枚举右端点为 \(i\) 的区间。
发现了什么?我们本来要一个一个给 \(f[i][0\sim l_i-1]\) 加上 \(w_i\),这样是 \(O(n)\) 的,但是现在我们可以把它看成区间加,即,将 \([0,l_i-1]\) 加上 \(w_i\)
用线段树维护它,是 \(O(\log n)\) 的!
最后把 \(f[i][i]\) 修改为 \(ans\) 即可。
fr(i,1,n){
mdf(1,0,n,0,i-1,-a[i]);
for(ll j=0;j<v[i].size();j++) mdf(1,0,n,0,q[v[i][j]].l-1,q[v[i][j]].w);
ans=max(ans,tree[1]);
mdf(1,0,n,i,i,ans);
}
CF474E Pillars
有点类似求 LIS,设 \(f_i\) 为前 \(i\) 个数中,选第 \(i\) 个数的答案(最大长度)
然后怎么转移呢?它可以由 \(f_j\) 转移过来,当且仅当 \(|a_i-a_j|\geq d,1 \leq j \leq i\)
具体来说,\(f_i=\displaystyle\max_{1 \leq j \leq i}^{|a_i-a_j|\geq d}{f_j}+1\),时间 \(O(n^2)\),考虑如何优化。
将 \(a_i\) 离散化,建一颗维护 \(a_i\)(值域)的线段树,假设 \(|a_i-a_j|\geq d\) 这个式子成立的边界是 \(l_i\) 与 \(r_i\)
什么意思?就是 \(a_j\) 在 \([1,l_i]\) 或 \([r_i,n]\) 里面的 \(j\) 都可以转移到 \(f_i\)(离散化后)
然后就简单了。用线段树维护答案最大值,求 \(f_i\) 的时候相当于求区间 \(\max\),每次在 \(a_i\) 处更新 \(f_i\)
fr(i,1,n){
node res=query(1,1,cnt,1,pl[i])+query(1,1,cnt,pr[i],cnt);
f[i]=res.maxx+1;
lst[i]=res.p;
mdf(1,1,cnt,a[i],i,f[i]);
}
P3431 [POI2005]AUT-The Bus
题目描述(这题翻译过烂)
一个 \(n \times m\) 的矩阵,\(k\) 个点有 \(p_i\) 个人,一辆大巴车从 \((1,1)\) 走到 \((n,m)\),只能往下或往右,求最多能接多少人。
\(n,m \leq 10^9,k \leq 10^5\)
假设 \(n,m \leq 10^3\),我该怎么做?直接对每个点 dp,\(f[i][j]=\max(f[i-1][j],f[i][j-1])+p[i][j]\)
那 \(n,m \leq 10^9\) 怎么做?离散化即可。
再来,假设 \(k \leq 10^3\),我该怎么做?
\(f_i=\displaystyle\max_{j}^{x_j \leq x_i,y_j \leq y_i}{f_j}+p_i\)
直接暴力枚举 \(k\) 个点进行转移。
现在考虑怎么优化。
首先对第一维 \(x_i\) 进行排序,那就只用考虑 \(y_j \leq y_i\) 了。
离散化 \(y_i\) 后以它建一颗线段树,每次即查 \([1,y_i]\) 上的 \(\max\)
然后再把 \(f_i\) 更新线段树里的 \(y_i\)
fr(i,1,k){
f[i]=query(1,1,cnt,1,a[i].y)+a[i].p;
mdf(1,1,cnt,a[i].y,f[i]);
}
CF597C Subsequences
先不管数据范围,思考如何暴力 dp。
一个非常非常非常暴力的做法就是设 \(f[i][j][s]\) 为前 \(i\) 个数中,LIS 长度为 \(j\) 且末尾为 \(s\) 的子序列个数。
如何转移?分两种情况:
-
\(s \neq a_i\):\(f[i][j][s]=f[i-1][j][s]\)
-
\(s=a_i\):\(f[i][j][s]=f[i-1][j][s]+\displaystyle\sum_{ss=1}^{s}{f[i-1][j-1][ss]}\)
还是考虑如何用线段树优化。
发现 \(k+1\) 很小,直接暴力开 \(k+1\) 颗线段树,然后以 LIS 末尾数字 \(s\) 为下标建线段树。
枚举 \(i\),对于第一种情况,不用管;第二种情况,相当于是在原来的基础上加上 \(\displaystyle\sum_{ss=1}^{s}{f[i-1][j-1][ss]}\)
这个东西直接在 \(j-1\) 这颗线段树里面查 \([1,s]\) 的和即可。
一点细节:倒序枚举 \(j\),原因如 01 背包,不要影响后面的答案。
注意特判 \(j=1\),这个时候序列个数是 \(1\)。
fr(i,1,n){
pfr(j,k+1,1){
if(j==1) tmp=1;
else tmp=query(rt[j-1],1,n,1,a[i]);
mdf(rt[j],1,n,a[i],tmp);
}
}
Upd:发现没有题解和我一样思路,可以水一篇题解了。
CF833B The Bakery
大概跟上题同样思路,设 \(f[i][j][s]\) 为前 \(i\) 个数中,最后一段是 \([j,i]\),然后共有 \(s\) 段的答案,\(pre_i\) 为上一个 \(a_i\) 的位置。
得出转移方程:
-
\(1 \leq j \leq pre_i\):\(f[i][j][s]=f[i-1][j][s]\)
-
\(pre_i<j<i\):\(f[i][j][s]=f[i-1][j][s]+1\)
-
\(j=i\):\(f[i][j][s]=f[i-1][1\sim j-1][s-1]+1\)
然后用线段树优化,考虑到 \(k\) 很小,直接暴力开 \(k\) 颗线段树,然后以 \(j\) 为下标。
枚举 \(i\),再枚举 \(s\),对于第一种情况直接忽略;第二种情况相当于区间加,第三种情况相当于区间查询。
注意倒序枚举 \(s\),当 \(s=1\) 的时候只能有 \([1,i]\) 这一段。
fr(i,1,n){
pfr(s,k,1){
if(s==1) add(rt[s],1,n,pre[i]+1,min(1ll,i-1));
else add(rt[s],1,n,pre[i]+1,i-1);
if(s==1) tmp=0;
else tmp=query(rt[s-1],1,n,1,i-1);
mdf(rt[s],1,n,i,tmp+1);
}
}
CF1304F2 Animal Observation (hard version)
感谢 \(\color{red}{ExplodingKonjac}\) 的供题。
很明显的 dp,设 \(f[i][j]\) 为第 \(i\) 天选择以 \((i,j)\) 为左上角的矩形(录像),前 \(i\) 天的答案。
然后 \(f[i][j]\) 可以由上一行的任意矩形(录像)转移过来,只是重合部分不一样。尽管使用前缀和优化,这样的时间复杂度还是 \(O(nm^2)\)
可不可以去掉对上一行,每个矩形(录像)都算一次重合部分的操作呢?
假设我已经知道了 \(f[i][j]\) 的情况下,我要把矩形往右移一个,也就是 \(j\) 加一,会发生什么变化?
\(f[i-1][j-k]\sim f[i-1][j-1]\) 的矩形重合部分,都减去了 \(a[i][j]\)。\(f[i-1][j]\sim f[i-1][j+k-1]\) 的矩形重合部分,都加了 \(a[i][j+k-1]\)。
然后这个东西用线段树维护即可。
对每一行,暴力求出 \(f[i][1]\),然后从左到右算答案。
fr(i,2,n){
fr(j,1,m-k+1) b[j]=f[i-1][j]-sum[i][k]+sum[i][min(k,j-1)];
build(1,1,m);
f[i][1]=tree[1]+cnm[i][1];
fr(j,2,m-k+1){
mdf(1,1,m,max(1ll,j-k),j-1,a[i][j-1]);
mdf(1,1,m,j,min(j+k-1,m),-a[i][j+k-1]);
f[i][j]=tree[1]+cnm[i][j];
}
}
CF960F Pathwalks
看到图,看到最长路径,难道是一个图上的 dp?
往这个地方想了一会,发现没什么思路。
重新看清楚题:编号与权值严格递增,编号是输入顺序的编号。
也就是说,我的路径编号一定是从小到大的。换个思路,在这上面 dp,就很简单了。
设 \(f_i\) 为前 \(i\) 条边中,选第 \(i\) 条的答案。
因为第 \(i\) 条边是 \(u_i \rightarrow v_i\),我要从某条边转移过来,必须这条边的终点是 \(u_i\)。
所以我要满足这些条件:
-
\(j<i\)
-
\(v_j=u_i\)
-
\(w_j<w_i\)
然后 \(f_i=\max{f_j}+1\)
第一个条件直接按顺序枚举 \(i\) 没掉,关键在后两个。
发现第二个貌似好弄一些,于是对每一个点开一颗线段树(当然动态开点),相当于查 \(u_i\) 这颗线段树。
然后以 \(w_i\) 为下标,相当于查 \(u_i\) 这颗线段树内,\([0,w_i-1]\) 的最小值。
最后记得用 \(f_i\) 更新 \(v_i\) 这颗线段树上的 \(w_i\) 位置。
fr(i,1,m){
f[i]=query(rt[e[i].u],0,1e5,0,e[i].w-1)+1;
mdf(rt[e[i].v],0,1e5,e[i].w,f[i]);
}
[USACO12OPEN]Bookshelf G
有个很假的 dp,就是设 \(f_{i,j}\) 为前 \(i\) 本书中,第 \(i\) 本所在架子目前宽度是 \(j\) 的答案。
但 \(L \leq 10^9\),无论时间还是空间都会炸,得想办法弄掉一维。
第一维肯定要保留,于是设 \(f_i\) 为前 \(i\) 本书的答案。考虑怎么枚举原先的第二维。
首先,第 \(i\) 本书所在的架子一定是连续的一段书,即 \((j,i]\)。为什么是左开?因为这样会好处理很多。(你也可以试着用闭区间)
然后我们必须满足 \(w_{j+1} + \cdots + w_i \leq L\),这个东西可以用前缀和预处理,即 \(sum_i-sum_j\leq L\)
再移一下项:\(sum_j \geq sum_i-L\),这个东西在枚举 \(i\) 的时候直接 \(lower\_bound\) 一下就行了。
所以 \(f_i = \displaystyle\min_{j}{(f_j+\max\{h_{j+1},\cdots ,h_i\})}\)
看,左开的好处体现出来了。
暴力转移 \(O(n^2)\),已经比之前优秀多了。但能不能继续优化呢?
观察这个式子,最难处理的就是 \(\max\{h_{j+1},\cdots ,h_i\}\)。
有个很显然的性质,就是这个东西随着 \(j\) 递增而递减。然后又是考虑 \(i\) 变成 \(i+1\) 的时候会有什么变化。
它能够产生影响,说明 \(h_i\) 更新了某些 \(\max\{h_{j+1},\cdots ,h_i\}\),即 \(h_i\) 比 \(h_{j+1} \cdots h_{i-1}\) 都要大。
但是当其中一个 \(h_k\) 比 \(h_i\) 要大时,\(h_i\) 就不能产生影响。
所以预处理出离 \(i\) 最近的比 \(h_i\) 大的位置 \(lst_i\),所有 \(lst_i \leq j<i\) 的 \(\max\{h_{j+1},\cdots ,h_i\}\) 就是 \(h_i\)。剩下的不变。
然后用线段树维护,处理一些细节,没了。
fr(i,1,n){
mdfmax(1,0,n,lst[i],i-1,h[i]);
ll tmp=lower_bound(sum,sum+n+1,sum[i]-L)-sum;
f[i]=query(1,0,n,tmp,i-1);
mdf(1,0,n,i,f[i]);
}
[TJOI2011]书架
跟上题一样,不再赘述。
[NWRRC2015]Journey to the “The World’s Start”
题目描述(这题翻译过烂)
你要从第 \(1\) 个点坐地铁到第 \(n\) 个点,有 \(n-1\) 张车票,每一张车票的乘车范围为 \(r_i=i\),并且有价格 \(p_i\)。乘车范围 \(r_i\) 表示你能从 \(i\) 点坐到 \([i-r_i,i+r_i]\) 内的点下车(不越界),然后再上车。
在第 \(i\) 个点下车的所花时间是 \(d_i\),第 \(1\) 和 \(n\) 个点下车不花时间,从一个点坐到另一个点需要 \(1\) 单位时间(如果 \(1\) 坐到 \(4\),需要 \(3\) 单位时间)。你需要在 \(t\) 时间内到达 \(n\),且只能用一种车票(可以用无数次),求车票费用最小值。
例如样例就是选择了 \(r_i=2\) 的车票,然后 \(1 \rightarrow 2 \rightarrow 4\)
如果要从 \(1\) 坐到 \(n\),则无论如何都要 \(n-1\) 单位时间。所以先让 \(t\) 减去 \(n-1\)
有个很明显的单调性,就是如果我 \(r_k\) 这个范围的车票可行,那么 \([r_k,n-1]\) 范围的车票都可行。
所以我只用找出来最小满足条件的 \(r_k\),那么 \([r_k,n-1]\) 都可行,答案便是 \(\displaystyle\min_{i=k}^{n-1}{p_i}\)
这个东西可以用二分找,现在的关键便是如何求第 \(k\) 张车票的最短时间。
假设目前用的是第 \(k\) 张车票,范围是 \(r_k=k\)
首先我只能坐地铁到后面的点,不然肯定不优。
然后想办法各种各样的贪心,发现都是错的(有兴趣的可以试一下能不能做)。于是开始想如何 dp。
设 \(f_i\) 为在 \(i\) 点下车的答案(最小值)。
根据前面的条件, \(f_i=\displaystyle\min_{j=\max(1,i-k)}^{i-1}f_j+d_i\)
然后 \(f_n\) 即为所求。
如果我们暴力求解,这样时间是 \(O(n^2)\) 的,带上二分变成了 \(O(n^2\log n)\),会 T 掉。
观察式子,极其简单,只有一个区间求 \(\min\)。
用一个线段树优化一下,就没了。
时间复杂度 \(O(n\log ^2n)\)
bl chk(ll k){
fr(i,2,n){
f[i]=query(1,1,n,max(1ll,i-k),i-1)+d[i];
mdf(1,1,n,i,f[i]);
}
return f[n]<=t;
}
Upd:发现此题没题解,去水一篇了。
练习题
我不会告诉你是因为我还没打