League of Legends | 2021牛客暑期多校训练营2 有
n
n
n 段区间,第
i
i
i 段为
[
L
i
,
R
i
)
[L_i,R_i)
[Li,Ri) 把它们分成
k
k
k 组,第
i
i
i 组的贡献为这组中线段的交的长度(要求非空) 问你总贡献最大是多少
1
≤
k
≤
n
≤
5000
1\le k\le n\le 5000
1≤k≤n≤5000
0
≤
L
i
<
R
i
<
1
0
5
0\le L_i<R_i<10^5
0≤Li<Ri<105
思路
首先可能会想到一个二维
d
p
dp
dp ,设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示前
i
i
i 条线段分成
j
j
j 组的最大贡献,已经是
O
(
n
k
)
O(nk)
O(nk) 了 但是我们不知道第
j
j
j 组的线段交的左右边界如何,也存不下这些状态了 然后貌似就卡在这里了,是因为我们想的线段之间的关系比较复杂
如果有线段
i
,
j
i,j
i,j 满足
i
∩
j
=
i
i\cap j=i
i∩j=i,那么显然线段
j
j
j 包含了线段
i
i
i 假设我们线段
i
i
i 放在组
x
x
x ,我们对于线段
j
j
j ,有两种最优策略: 一种:新开一组,单独放线段
j
j
j ,答案会增加
j
j
j 长度的贡献 另一种:把线段
j
j
j 放在组
x
x
x ,答案不会改变 (想一想为什么) 若把线段
j
j
j 放在别的组,有可能导致那组的线段交会长度变短,自然更劣
所以我们把所有的包含别的线段的大线段拿出来,只考虑小线段分组的情况 怎么拿呢?看代码:
/**
seg[i] : 原线段
sml[i] : 小线段
big[i] : 大线段的长
*/structnode{int l,r;
bool operator <(const node &ND)const{if(l != ND.l)return l < ND.l;// 注意重载规则return r > ND.r;}}seg[MAX],sml[MAX];int big[MAX];int id1,id2;intmain(){
IOS;int n,k;cin >> n >> k;for(int i =0;i < n;++i)
cin >> seg[i].l >> seg[i].r;sort(seg,seg+n);int you = INF;for(int i = n-1;~i;--i){// 我们倒着取,记录目前线段的最右边界if(seg[i].r >= you)big[id2++]= seg[i].r - seg[i].l;// 右区间大于最右边界,是包含别的线段的大线段else{
sml[++id1]= seg[i];
you = seg[i].r;}}}
第一个难点过去了,接下来是第二个难点。 我们对小线段排序一下,按照左端点升序即可 因为我们拿出来的所有小线段都满足两两不包含,即
∀
i
,
j
,
i
<
j
\forall i,j,i<j
∀i,j,i<j 我们满足
L
i
<
L
i
,
R
i
<
R
j
L_i<L_i,R_i<R_j
Li<Li,Ri<Rj 这对我们后续的操作都大有裨益 接下来是动态规划了。我们设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示前
i
i
i 条线段分
j
j
j 组的最大贡献,式子也很好转移了:
d
p
[
i
]
[
j
]
=
max
{
d
p
[
k
]
[
j
−
1
]
+
s
m
l
[
k
+
1
]
.
r
−
s
m
l
[
i
]
.
l
∣
i
>
k
+
1
,
s
m
l
[
i
]
∩
s
m
l
[
k
+
1
]
≠
∅
}
dp[i][j]=\max\{ dp[k][j-1] + sml[k+1].r - sml[i].l \Big| i>k+1,sml[i]\cap sml[k+1] \ne \varnothing\}
dp[i][j]=max{dp[k][j−1]+sml[k+1].r−sml[i].l∣∣∣i>k+1,sml[i]∩sml[k+1]=∅} 我们可以稍微处理一下式子:
d
p
[
i
]
[
j
]
=
max
{
d
p
[
k
−
1
]
[
j
−
1
]
+
s
m
l
[
k
]
.
r
}
−
s
m
l
[
i
]
.
l
∣
i
>
k
,
s
m
l
[
i
]
∩
s
m
l
[
k
]
≠
∅
dp[i][j]=\max\{ dp[k-1][j-1] + sml[k].r \} - sml[i].l \qquad \Big |\ i> k,sml[i]\cap sml[k]\ne \varnothing
dp[i][j]=max{dp[k−1][j−1]+sml[k].r}−sml[i].l∣∣∣i>k,sml[i]∩sml[k]=∅ 简单讲,就是
k
∼
i
k\sim i
k∼i 的线段都分成一组,区间交就是
s
m
l
[
k
]
.
r
−
s
m
l
[
i
]
.
l
sml[k].r - sml[i].l
sml[k].r−sml[i].l
问题来了:怎么快速的去算呢?容易想到,这个满足 单调队列 的性质,即合法区间一定是连续的一段,类似尺取 然后注意到,对于分
k
k
k 组,我们需要计算的最大值中是
k
−
1
k-1
k−1 组 所以我们开
k
k
k 个单调队列去存就好 另外,就像背包一样,我们组数倒着跑,不然某一段区间可能被连续贡献
还有一个点。对于那些大线段,我们怎么处理? 假设我们所有的小线段分成了
k
−
i
k-i
k−i 组,那么大线段要分
i
i
i 组,我们去拿最长的
i
i
i 个大线段分这
i
i
i 组即可,因为别的可以放到前面的小线段的组中,对答案的贡献都是
0
0
0 然后对所有情况取
max
\max
max 即可
代码
时间复杂度:
O
(
n
k
)
O(nk)
O(nk)
#include<bits/stdc++.h>#defineIOSios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
using namespace std;typedeflonglong ll;voidshow(){std::cerr << endl;}template<typename T,typename... Args>voidshow(T x,Args... args){std::cerr <<"[ "<< x <<" ] , ";show(args...);}constint MAX =5e3+50;constint MOD =1e9+7;constint INF =0x3f3f3f3f;const ll LINF =0x3f3f3f3f3f3f3f3f;constdouble EPS =1e-5;structnode{int l,r;
bool operator <(const node &ND)const{if(l != ND.l)return l < ND.l;return r > ND.r;}}seg[MAX],sml[MAX];int big[MAX];int id1,id2;int dp[MAX][MAX];
deque<int>Q[MAX];
bool capEmpty(int x,int y){return sml[y].l >= sml[x].r;//return l >= r;}intmain(){
IOS;int n,k;cin >> n >> k;for(int i =0;i < n;++i)
cin >> seg[i].l >> seg[i].r;sort(seg,seg+n);int you = INF;for(int i = n-1;~i;--i){if(seg[i].r >= you)big[id2++]= seg[i].r - seg[i].l;else{
sml[++id1]= seg[i];
you = seg[i].r;}}sort(sml+1,sml+id1+1);sort(big,big+id2,greater<int>());memset(dp,-1,sizeof(dp));// -1 表示取不到
dp[0][0]=0;
Q[0].push_back(1);// 初始状态没有就进不去了for(int i =1;i <= id1;++i){for(int j = k;j >=1;--j){while(Q[j-1].size()&&capEmpty(Q[j-1].front(),i))Q[j-1].pop_front();if(Q[j-1].size())dp[i][j]= dp[Q[j-1].front()-1][j-1]+ sml[Q[j-1].front()].r - sml[i].l;if(~dp[i][j]){while(Q[j].size()&& dp[i][j]+ sml[i+1].r >= dp[Q[j].back()-1][j]+ sml[Q[j].back()].r)Q[j].pop_back();
Q[j].push_back(i+1);}}}
ll ans =0;
ll sum =0;for(int i =0;i <= id2 && k - i >=0;++i){if(~dp[id1][k-i])
ans =max(ans,dp[id1][k - i]+ sum);
sum += big[i];}
cout << ans;return0;}