好久都懒得写题解,直到今天跟着题解写完这道题,发现这道题也太太太太太妙了,被美妙的做法震惊到了,来记录一下。
题目大意:
对于一颗区间范围为n的线段树,你可以对每个区间的断点进行重新分配,如区间
l
,
r
l, r
l,r 可以在
[
l
,
r
)
[l, r)
[l,r) 中任选一个k作为断点使得两个儿子分别为
[
l
,
k
]
[l, k]
[l,k]
[
k
+
1
,
r
]
[k+1, r]
[k+1,r]。
给m次询问,每次询问为一个区间,求在线段树查询时总共需要经过的最少节点个数。
数据范围n 500,m 2e5
这题精妙的地方便在于对线段树节点的理解上,首先我们知道在线段树上进行 [ l , r ] [l, r] [l,r] 区间查询时,对于每个区间 [ L , R ] [L, R] [L,R] ,如果 [ L , R ] [L, R] [L,R] 与 [ l , r ] [l, r] [l,r] 有交错区间或且 [ L , R ] [L, R] [L,R] 不被 [ l , r ] [l, r] [l,r] 包含,那么这个节点一定被经过一次,剩下的便是被 [ l , r ] [l, r] [l,r] 包含的区间,这样的区间一定是一次访问的最后一层。
首先,线段树的一个节点 [ L , R ] [L, R] [L,R] 的子树大小为 ( R − L + 1 ) ∗ 2 − 1 (R-L+1)*2-1 (R−L+1)∗2−1,其中叶节点个数为 ( R − L + 1 ) (R-L+1) (R−L+1),剩下的非叶节点个数为 ( R − L ) (R-L) (R−L)。剩下的被 [ l , r ] [l, r] [l,r] 包含的区间无非是叶节点以及非叶节点两种,他们的数量差值恰好为1。
如此一来,我们就可以将所有被 [ l , r ] [l, r] [l,r] 包含的非叶节点贡献记为 − 1 -1 −1,所有被 [ l , r ] [l, r] [l,r] 包含的叶节点贡献记为 + 1 +1 +1, 与 [ l , r ] [l, r] [l,r] 有交错区间且不被 [ l , r ] [l, r] [l,r] 包含的节点贡献记为 + 1 +1 +1,这样所有被包含的节点的子树贡献和总为1,而经过的有交错的节点贡献也为1。
这样,我们就可以将所有种类的区间贡献处理出来,剩下的就是一个区间合并问题了,可以用一个显然的dp解决。
#include <bits/stdc++.h>
using namespace std;
const int maxn=505;
long long dp[maxn][maxn],a[maxn][maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++){
int l,r;
cin>>l>>r;
for(int j=1;j<=r;j++){
if(j<l) a[j][l]++;
else{
a[j][r+1]+=2;
a[j][j]++;
a[j][j+1]-=2;
}
}
}
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
a[i][j]+=a[i][j-1],dp[i][i]=a[i][i];
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+a[i][j]);
}
}
}
cout<<dp[1][n]<<endl;
}