线段树 & 线段树合并优化 DP

文章介绍了如何使用线段树优化动态规划,通过例题说明了在线段树中进行区间最大值查询、区间加法和单点修改等操作,以提高算法效率。文章详细分析了导弹拦截、奇怪函数和歌姬故事等例题的解题思路,展示了如何将区间操作转化为线段树操作,从而降低时间复杂度。
摘要由CSDN通过智能技术生成

线段树优化 DP

有一些 DP 的初始化和转移操作可以转化为序列上 / 值域上的区间操作 / 区间查询问题,可以用线段树加速这些操作。

例题 1. [NOIP1999 普及组] 导弹拦截 - 洛谷

求序列的最长不上升 / 最长上升子序列, 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ a i ≤ 5 × 1 0 4 1 \le a_i \le 5\times10^4 1ai5×104


以最长不上升子序列为例。

朴素转移方程: f i = max ⁡ j = 0 i − 1 ( f j + 1 ) [ a j ≥ a i ∨ j = 0 ] f_i = \max \limits _{j=0} ^{i - 1} (f_j+1)[a_j \ge a_i \lor j=0 ] fi=j=0maxi1(fj+1)[ajaij=0] f i f_i fi 表示以 a i a_i ai 结尾的最长子序列,从 f 0 f_0 f0 转移表示作为开头。这个 DP 时间复杂度为 O ( n 2 ) \mathcal{O} (n^2) O(n2)

考虑优化。枚举到 k k k 时,如果有 i , j ≤ k i,j \le k i,jk a i = a j a_i = a_j ai=aj f i < f j f_i < f_j fi<fj,那么 f i f_i fi 显然没用。由于值域很小,可以用 g i g_i gi 记录 max ⁡ a j = i f j \max \limits _{a_j=i} f_j aj=imaxfj,转移方程变为 f i = max ⁡ j = a i V g j f_i = \max \limits _{j=a_i} ^{V} g_j fi=j=aimaxVgj,在计算 f i f_i fi 的同时更新 g a i g_{a_i} gai 即可,时间复杂度 O ( n V ) \mathcal{O}(nV) O(nV)

发现转移时,相当于在 [ 0 , V ] [0,V] [0,V] 中求了一次后缀最大值,可以用数据结构维护区间最大值,更新 g a i g_{a_i} gai 的操作转化为单点修改,可以用线段树或其他数据结构维护,时间复杂度 O ( n log ⁡ V ) \mathcal{O}(n\log V) O(nlogV),可以通过。

例题 2. Problem - 1334F - Codeforces

翻译:Strange Function - 洛谷

定义函数 f f f f ( x ) f(x) f(x) 为所有满足 x i > x 1 , 2 , ⋯   , i − 1 x_i>x_{1,2,\cdots,i-1} xi>x1,2,,i1 x i x_i xi 组成的序列,例如 f [ 3 , 1 , 2 , 7 , 7 , 3 , 6 , 7 , 8 ] = [ 3 , 7 , 8 ] f[3,1,2,7,7,3,6,7,8]=[3,7,8] f[3,1,2,7,7,3,6,7,8]=[3,7,8]

给出两个序列 a , b a,b a,b,你可以删掉 a a a 中的一些元素。删掉 a i a_i ai 的代价为 p i p_i pi。你需要求出最小代价使得 f ( a ) = b f(a)=b f(a)=b 或给出无解。

1 ≤ ∣ a ∣ ≤ 5 × 1 0 5 1 \le |a| \le 5\times 10^5 1a5×105 b i − 1 < b i b_{i-1} < b_i bi1<bi


设计状态 f i , j f_{i,j} fi,j 表示现在考虑到第 i i i 个序列 a a a 中的元素,考虑完元素 i i i 后新序列的目前最大值(有可能不在目前的新序列中)为 b j b_j bj,目前代价最小为 f i , j f_{i,j} fi,j

b k ≤ a i < b k + 1 b_{k} \le a_i < b_{k+1} bkai<bk+1

先假设 b j b_j bj 已经 / 将来一定会取到。

p i > 0 p_i>0 pi>0 时,若 b j ≤ a i b_{j} \le a_i bjai j ≤ k j \le k jk,为了不改变目前最大值, a i a_i ai 必须删除, f i , j ← f i − 1 , j + p i f_{i,j} \leftarrow f_{i-1,j}+p_i fi,jfi1,j+pi;否则, a i a_i ai 删不删都可以, f i , j ← f i − 1 , j f_{i,j} \leftarrow f_{i-1,j} fi,jfi1,j

p i < 0 p_i < 0 pi<0 时, a i a_i ai 删除更优, f i , j ← f i − 1 , j + p i f_{i,j} \leftarrow f_{i-1,j}+p_i fi,jfi1,j+pi

为了保证最后 b j b_j bj 可以取到,当 a i = b j a_i=b_j ai=bj 时, a i a_i ai 不能删除, f i , j = min ⁡ v = 0 j f i − 1 , v = min ⁡ v = 0 j f i , v − p i f_{i,j} = \min \limits _{v=0} ^j f_{i-1,v} = \min \limits _{v=0} ^{j} f_{i,v} - p_i fi,j=v=0minjfi1,v=v=0minjfi,vpi(减 p i p_i pi 是因为 v ≤ j v \le j vj,在上面加上了 p i p_i pi,要把 p i p_i pi 减回来)。

最终答案即为 f ∣ a ∣ , ∣ b ∣ f_{|a|,|b|} fa,b

发现上面的几个式子可以转化为区间加、区间查询和单点修改,可以用线段树维护。时间复杂度 O ( ∣ a ∣ log ⁡ ∣ b ∣ ) \mathcal{O}(|a|\log |b|) O(alogb)

DP 的主要代码:

signed main()
{
    scanf("%lld",&n);
    for (int i = 1;i <= n;i ++) scanf("%lld",a + i);
    for (int i = 1;i <= n;i ++) scanf("%lld",p + i);
    scanf("%lld",&m);
    for (int i = 1;i <= m;i ++) scanf("%lld",b + i),pl[b[i]] = i;
    build(1,0,m);
    for (int i = 1;i <= n;i ++)
    {
        int x = lower_bound(b + 1,b + m + 1,a[i]) - b;
        if (p[i] > 0) add(1,0,x - 1,p[i]);
        else add(1,0,m,p[i]);
        if (pl[a[i]]) upd(1,pl[a[i]],query(1,pl[a[i]] - 1) - p[i]);
    }
    // add 为区间加,query 为区间查询,upd 为单点修改
    if (query(1,m) < 1e15) printf("YES\n%lld\n",query(1,m));
    else printf("NO\n");
}

例题 3. 某位歌姬的故事 - 洛谷 / Ex - Max Limited Sequence

其中前一道题实际上可以不用线段树优化 DP。

构造一个长度为 n n n 的整数序列 a a a,要求 1 ≤ a i ≤ A 1\le a_i \le A 1aiA,还有 Q Q Q 条形如 max ⁡ { a l i , a l i + 1 , ⋯   , a r i } = m i \max \{a_{l_i},a_{l_i+1},\cdots,a_{r_i}\}=m_i max{ali,ali+1,,ari}=mi 的限制,问有多少种构造方法。

要求时间复杂度为 O ( Q log ⁡ Q ) \mathcal{O}(Q\log Q) O(QlogQ),空间线性。


先求出每个位置上的数最大可以填多少,设为 b b b,顺便判断是否有解:限制 i i i 可能被满足当且仅当 max ⁡ j = l i r i b j = m i \max \limits _{j=l_i} ^{r_i} b_j = m_i j=limaxribj=mi

类似[FJOI2017]矩阵填数 - 洛谷这道题, b i b_i bi 不同的两个位置的填法互不相关,可以依次求 b i = x b_i=x bi=x 的所有位置的填法,再乘起来。

把位置、限制按 b b b 分类,分别存起来,所有 b i = x b_i=x bi=x 的位置把整个序列分成若干个左闭右开的区间。现在求 b i = x b_i=x bi=x 的位置的填法数。注意 DP 时一个点不仅代表这一个点填什么,还代表了一个左闭右开的区间,设 i i i 代表的区间长度为 l e n i len_i leni

设计状态 f i , j f_{i,j} fi,j 表示现在填到第 i i i 个位置,上一个填 x x x 的位置为 j j j,填法有 f i , j f_{i,j} fi,j 种。

一个观察:右端点在 i i i 代表的区间中的所有限制,只要满足了左端点最靠右的一个,剩下的所有都满足了。因此只需要考虑左端点最靠右的一个询问,设这个左端点为 y y y。则 y y y i i i 中至少要有一个填 x x x

如果 i i i 不填 x x x,则 y y y i − 1 i-1 i1 中至少有一个 x x x,所以 ∀ j : 0 ≤ j < y ,   f i , j ← 0 \forall j: 0 \le j < y,\ f_{i,j}\leftarrow 0 j:0j<y, fi,j0 ∀ j : y ≤ j < i ,   f i , j ← f i − 1 , j × ( x − 1 ) l e n i \forall j : y \le j < i, \ f_{i,j} \leftarrow f_{i-1,j} \times (x-1)^{len_i} j:yj<i, fi,jfi1,j×(x1)leni

如果填了 x x x,则 f i , i ← ∑ j = 0 i − 1 f i − 1 , j × [ x l e n i − ( x − 1 ) l e n i ] f_{i,i}\leftarrow \sum \limits _{j=0} ^{i-1} f_{i-1,j} \times [x^{len_i} - (x-1)^{len_i}] fi,ij=0i1fi1,j×[xleni(x1)leni]

这里,位置编号是从 1 1 1 开始的, f i , 0 f_{i,0} fi,0 表示一个 x x x 都没有。

答案即为 ∑ i = 0 l a s t f l a s t , i \sum \limits _{i=0} ^{last} f_{last,i} i=0lastflast,i

考虑优化,发现上面的转移可以转化为区间推平、区间乘、区间查询和单点修改,可以用线段树维护。

在实现时,区间查询的结果也可以用另外一个数组记录,看起来好看一点。

我的代码写得非常丑,好看的正解代码可以在洛谷 / AT 上找 qwq

AT 那道题和这题是一样的,只是没有多测。

其他题目

https://www.luogu.com.cn/problem/P2605

待补充

线段树合并优化 DP

在一些树上 DP 的题目中,需要把父亲和儿子的 DP 信息合并起来,而合并的过程可以转化为线段树合并操作。

例题 1. [PKUWC2018] Minimax - 洛谷

C C C 有一棵 n n n 个结点的有根树,根是 1 1 1 号结点,且每个结点最多有两个子结点。

定义结点 x x x 的权值为:

1.若 x x x 没有子结点,那么它的权值会在输入里给出,保证这类点中每个结点的权值互不相同

2.若 x x x 有子结点,那么它的权值有 p x p_x px 的概率是它的子结点的权值的最大值,有 1 − p x 1-p_x 1px 的概率是它的子结点的权值的最小值。

现在小 C C C 想知道,假设 1 1 1 号结点的权值有 m m m 种可能性,权值第 i i i的可能性的权值是 V i V_i Vi,它的概率为 D i ( D i > 0 ) D_i(D_i>0) Di(Di>0),求:

∑ i = 1 m i ⋅ V i ⋅ D i 2 \sum_{i=1}^{m}i\cdot V_i\cdot D_i^2 i=1miViDi2

你需要输出答案对 998244353 998244353 998244353 取模的值。


一眼树形 DP。设计状态 f x , i f_{x,i} fx,i 表示节点 x x x 取到权值 i i i 的概率。

x x x 为叶子节点,则 f x , i ← [ v a l x = i ] v a l x f_{x,i} \leftarrow [val_x = i] val_x fx,i[valx=i]valx

x x x 只有一个儿子,则 f x , i ← f s o n x , i f_{x,i} \leftarrow f_{son_x,i} fx,ifsonx,i

否则,设 x x x 的两个儿子为 l s o n lson lson r s o n rson rson

f x , i ← p x ∑ j = 1 i − 1 f l s o n , j f r s o n , i + ( 1 − p x ) ∑ j = i + 1 V f l s o n , j f r s o n , i + p x ∑ j = 1 i − 1 f l s o n , i f r s o n , j + ( 1 − p x ) ∑ j = i + 1 V f l s o n , i f r s o n , j + f l s o n , i f r s o n , i f_{x,i} \leftarrow p_x\sum \limits _{j=1} ^{i-1} f_{lson,j} f_{rson,i} + (1-p_x)\sum \limits _{j=i+1} ^{V} f_{lson,j}f_{rson,i} +p_x\sum \limits _{j=1}^{i-1} f_{lson,i}f_{rson,j}+(1-p_x)\sum\limits_{j=i+1}^V f_{lson,i}f_{rson,j}+f_{lson,i}f_{rson,i} fx,ipxj=1i1flson,jfrson,i+(1px)j=i+1Vflson,jfrson,i+pxj=1i1flson,ifrson,j+(1px)j=i+1Vflson,ifrson,j+flson,ifrson,i

要怎么把线段树合并和这个东西结合起来呢?

x x x 为叶子节点就是单点修改,只有一个儿子就是 rt[x] = rt[sonx],主要问题是两个儿子的情况。

想一下线段树合并的过程:

int merge(int x,int y,int l,int r)
{
    if (!x || !y) return x + y; // 其中一个为空就直接返回
    // tree[x] <- tree[y]
    if (l == r) return x; // 没法往下分就返回
    tree[x].ls = merge(tree[x].ls,tree[y].ls,l,mid);
    tree[x].rs = merge(tree[x].rs,tree[y].rs,mid + 1,r); // 递归
    return x;
}

假设现在 f f f 数组已经成功地存在了两棵线段树里,要把 f r s o n f_{rson} frson 合并到 f l s o n f_{lson} flson 上作为 f x f_x fx

现在合并到了区间 [ l , r ] [l,r] [l,r]。如果 f r s o n f_{rson} frson [ l , r ] [l,r] [l,r] 没有值,即 y y y 为空,那么对于所有 i ∈ [ l , r ] i \in [l,r] i[l,r],和它的值有关的 f r s o n f_{rson} frson 中的值都在这个区间外面,与 i i i 本身无关,只与 [ l , r ] [l,r] [l,r] 有关。具体来说,上面转移方程中,第一项、第二项和最后一项都是 0 0 0,第三项变为 p x ∑ j = 1 l − 1 f l s o n , i f r s o n , j p_x\sum \limits _{j=1} ^{l-1} f_{lson,i}f_{rson,j} pxj=1l1flson,ifrson,j,第四项变为 ( 1 − p x ) ∑ j = r + 1 V f l s o n , i f r s o n , j (1-p_x)\sum \limits _{j=r+1}^V f_{lson,i}f_{rson,j} (1px)j=r+1Vflson,ifrson,j。发现实际上就是给 [ l , r ] [l,r] [l,r] 中的每个位置乘上 p x ∑ j = 1 l − 1 f r s o n , j + ( 1 − p x ) ∑ j = r + 1 V f r s o n , j p_x \sum \limits _{j=1}^{l-1}f_{rson,j}+(1-p_x)\sum \limits _{j=r+1}^Vf_{rson,j} pxj=1l1frson,j+(1px)j=r+1Vfrson,j,变成了区间乘。 x x x 为空时同理,因为 x x x y y y 的关系是对等的。发现要维护一些前缀和和后缀和,在下分时顺便更新之后传下去就行了。

如果 l = r l=r l=r,直接根据转移方程暴力合并就行了。在这道题中,由于点的权值互不相同,所以不会有这种情况。

否则往下继续分就行了。注意下分前先下传 tag,合并完儿子之后再 pushup。

合并的代码:

int merge(int x,int y,int l,int r,int xsum,int ysum,int P)
{
	if (!x && !y) return 0;
	if (!x || !y)
	{
		if (!x) swap(x,y),swap(xsum,ysum);
		tree[x].sum = 1ll * tree[x].sum * xsum % mod;
		tree[x].tg = 1ll * tree[x].tg * xsum % mod;
		return x;
	}
	pushdown(x),pushdown(y);
	int tmpx = tree[tree[x].ls].sum,tmpy = tree[tree[y].ls].sum;
	tree[x].ls = merge(tree[x].ls,tree[y].ls,l,mid,(xsum + 1ll * tree[tree[y].rs].sum * (1 - P + mod) % mod) % mod,(ysum + 1ll * tree[tree[x].rs].sum * (1 - P + mod) % mod) % mod,P);
	tree[x].rs = merge(tree[x].rs,tree[y].rs,mid + 1,r,(xsum + 1ll * tmpy * P % mod) % mod,(ysum + 1ll * tmpx * P % mod) % mod,P);
	pushup(x);
	return x;
}

剩下的都是动态开点线段树板子。

最后查询的时候,就是在根节点的线段树上单点查找。

例题 2. [NOI2020] 命运 - 洛谷

给定一棵树 T = ( V , E ) T = (V, E) T=(V,E) 和点对集合 Q ⊆ V × V \mathcal Q \subseteq V \times V QV×V ,满足对于所有 ( u , v ) ∈ Q (u, v) \in \mathcal Q (u,v)Q,都有 u ≠ v u \neq v u=v,并且 u u u v v v 在树 T T T 上的祖先。其中 V V V E E E 分别代表树 T T T 的结点集和边集。求有多少个不同的函数 f f f : E → { 0 , 1 } E \to \{0, 1\} E{0,1}(将每条边 e ∈ E e \in E eE f ( e ) f(e) f(e) 值置为 0 0 0 1 1 1),满足对于任何 ( u , v ) ∈ Q (u, v) \in \mathcal Q (u,v)Q,都存在 u u u v v v 路径上的一条边 e e e 使得 f ( e ) = 1 f(e) = 1 f(e)=1。由于答案可能非常大,你只需要输出结果对 998 , 244 , 353 998,244,353 998,244,353(一个素数)取模的结果。

1 ≤ n ≤ 5 × 1 0 5 1 \le n \le 5 \times 10 ^5 1n5×105 1 ≤ m ≤ 5 × 1 0 5 1 \le m \le 5 \times 10^5 1m5×105


这道题和前面讲的 某位歌姬的故事 这道题很像,这道题就是把那道题的部分分做法搬到了树上。

这道题也有一个观察:对于下端点相同的限制,上端点最深的满足,则这些限制都满足

设计状态 f i , j f_{i,j} fi,j 表示在节点 i i i 的子树中,还未被满足的限制中上端点最深的深度为 j j j,方案数为 f i , j f_{i,j} fi,j。根节点深度为 1 1 1

转移方程:

f x , i ← ∑ j = 0 i − 1 f x , i f s o n , j + ∑ j = 0 i − 1 f x , j f s o n , i + f x , i f x o n , i + f x , i ∑ j = 0 d e p x f s o n , j f_{x,i} \leftarrow \sum \limits _{j=0} ^{i - 1} f_{x,i}f_{son,j}+\sum\limits _{j=0} ^{i-1} f_{x,j}f_{son,i}+f_{x,i}f_{xon,i}+f_{x,i}\sum \limits _{j=0}^{dep_x}f_{son,j} fx,ij=0i1fx,ifson,j+j=0i1fx,jfson,i+fx,ifxon,i+fx,ij=0depxfson,j

前面三项是 e d g e ( x , s o n ) = 0 edge(x,son)=0 edge(x,son)=0,最后一项是填 1 1 1

类似上一道题,这题也可以用线段树合并优化转移。但要注意,这道题里 x x x s o n son son 的关系并不对等,要分别讨论左边为空和右边为空的情况。

合并代码:

int merge(int x,int y,int l,int r,int pre,int sonpre,int sondep)
{
//	printf("%d %d %d %d %d %d %d\n",x,y,l,r,pre,sonpre,sondep);
	if (!x && !y) return 0;
	if (!x)
	{
		swap(x,y);
		tree[x].sum = 1ll * tree[x].sum * pre % mod;
//		printf("sum %d\n",tree[x].sum);
		tree[x].tg = 1ll * tree[x].tg * pre % mod;
		return x;
	}
	if (!y)
	{
		tree[x].sum = 1ll * tree[x].sum * (sondep + sonpre) % mod;
		tree[x].tg = 1ll * tree[x].tg * (sondep + sonpre) % mod;
		return x;
 	}
 	if (l == r)
 	{
		tree[x].sum = (1ll * tree[x].sum * ((sondep + sonpre) % mod + tree[y].sum) % mod + 1ll * tree[y].sum * pre % mod) % mod;
//		printf("sum %d\n",tree[x].sum);
		return x;
	}
	pushdown(x),pushdown(y);
	int newpre = (pre + tree[tree[x].ls].sum) % mod;
	int newsonpre = (sonpre + tree[tree[y].ls].sum) % mod;
	tree[x].ls = merge(tree[x].ls,tree[y].ls,l,mid,pre,sonpre,sondep);
	tree[x].rs = merge(tree[x].rs,tree[y].rs,mid + 1,r,newpre,newsonpre,sondep);
	pushup(x);
	return x;
}

总结

遇到整个区间转移的问题,可以往线段树优化上考虑。(?)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值