君君演算法課堂-好題分享1
好題分享1
Problem Description
Tarzan 非常煩數軸因為數軸上的題總是難度非常大。不過他非常喜歡線段,因為有關線
段的題總是不難,諷刺的是在一個數軸上有 n 個線段,Tarzan 希望自己喜歡的東西和討厭的
東西不在一起,所以他要把這些線段分多次帶走,每一次帶走一組,最多能帶走 k 次。其實
就是要把這些線段分成至多 k 組,每次帶走一組,問題遠沒有那麼簡單,tarzan 還希望每次
選擇的線段組都很有相似性,我們定義一組線段的相似性是組內線段交集的長度,我們現在
想知道最多分成 k 個組帶走,Tarzan 最多能得到的相似性之和是多少?
Input format
第一行兩個整數 n 和 k。
接下來 n 行每行兩個整數 Li, Ri 表示線段的左右端點。
Output format
一行一個整數,表示最多能得到的相似性之和是多少。
Examples
input 1
5 3
5 10
4 11
6 9
10 30
20 40
output 1
43
input 2
5 3
5 11
16 22
14 20
10 20
6 10
output 2
18
input 3
7 3
1 9
2 9
2 10
5 15
3 14
14 18
16 20
output 3
21
Constrains and Notes
對於 20% 的資料滿足:\(n ≤ 8; k ≤ 5\)
對於 40% 的資料滿足:\(k, n ≤ 12\);
對於 70% 的資料滿足:\(n ≤ 100\)
對於 100% 的資料滿足:\(1 ≤ k ≤ n ≤ 6000, 1 ≤ Li < Ri ≤ 10^6\);
題解
對於 \(\%20\) 的資料,直接列舉每一個線段在哪一個集合裡面即可
時間複雜度:\(O(k^n)\)
對於 \(\%40\) 的資料,\(dfs\) 搜尋每一條線段在哪一個集合裡面,
進行適當剪枝後可以得到最高 \(40\) 分的部分分,
對於 \(\%70\) 的資料,首先,手玩樣例一番,收集性質如下:
①空集組(對答案無貢獻)最多隻有一組。
證明:若有\(k(k >= 2)\)組空集, 則在這些集合中找出前\(k-1\)條線段放入一個集合, 另外的線段放入剩下的一個集合可增加選出來的\(k-1\)
性質①得證。
②若沒有空集的話,可以再觀察到一個性質:
對於完全包含另一個線段B的線段A, 則B與A在一組可能會使答案更優(但不一定),不優的情況會在下文另行考慮。
證明:根據題意可得,對於給出的\(n\)條線段,每條線段都會屬於一個集合。
設長度為\(x\)的線段被長度為\(y(y > x)\)的線段包含。
因 \(x\)被\(y\)包含,所以\(x\)與\(y\)分到一組,則\(x\)所在集合對答案的貢獻最大為\(x\)。
而若\(x\)與不包含\(x\)的線段分到一組時,在小的方面來說答案不優(整體來看可能更優)。
性質②得證。
下面對於性質②的缺陷作考慮:
性質②是將\(x\)與\(y\)放到一個集合,那我們未考慮到的情況就是\(x\)與\(y\)相分離的情況。
我們考慮\(x\)與\(y\)相分離時怎樣最優。
顯然當\(y\)單獨一個集合時對答案的貢獻最大。
當然,上面情況成立的條件是仍有空的集合未被使用。
注:當產生包含關係後,\(y\)對集合已無貢獻。
所以我們才可以將\(x\)與\(y\)進行分離操作。
否則,演算法正確性無法得以保證。
此時,再結合性質①,對答案無貢獻的集合最多隻有一個。
我們便有了一種做法:
優先考慮不包含的情況,再將\(x\)與\(y\)進行分離操作,更新答案。
因分離線段操作(只考慮包含別的線段的線段)不會對現有答案產生影響。
所以我們可以進行DP預處理操作。
將線段排序,挑選所有出\(r\)遞增且\(l\)遞增(不包含)的線段。
此時顯然能DP。
設\(f[i][j]\)為前\(i\)條線段選了\(j\)個集合相似度之和。
注:這\(j\)個集合不能有空集,因為我們要給每個空集分發線段,更新答案。
否則答案不優。
考慮第\(j\)個集合放入第\(x+1\sim i\)條線段,
則\(f[i][j]=max(f[x][j-1]+r[x+1]-l[i]|r[x+1]>l[i])\)
轉化方程得:\(f[i][j]=max(f[x][j-1]+r[x+1]|r[x+1]>l[i])-l[i]\)
對於 \(\%100\) 的資料,在 \(\%70\) 的資料的基礎上進行單調佇列優化。
考慮我們選擇了多少集合,設為\(p\),
考慮用包含其它線段的線段來更新答案。
則此情況對答案的貢獻為
\(f[n][p]\)+包含其它線段的前\(k-p\)長線段長度之和。
再列舉\(p\)取最優值即可。
時間複雜度:\(O(n^2)\),在空間上可以使用滾動陣列優化。
code
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6e3 + 5;
int read() {
int x = 0, f = 1; char ch = getchar();
while(! isdigit(ch)) f = (ch == '-') ? -1 : 1, ch = getchar();
while(isdigit(ch)) x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
return x * f;
}
struct edge { int l, r; } t1[N], t2[N];
int n, k, ans, r, len[N], q[N], head, tail, maxl, tot, cnt, f[2][N];
bool cop(const edge &a, const edge &b) {
return (a.r == b.r) ? (a.l > b.l) : (a.r < b.r);
}
int main() {
n = read(); k = read();
for(int i = 1; i <= n; i ++)
t1[i].l = read(), t1[i].r = read(), len[i] = t1[i].r - t1[i].l;
sort(len + 1, len + n + 1);
for(int i = n - k + 2; i <= n; i ++) ans += len[i];//共前 k-1 長 (總字首和);
sort(t1 + 1, t1 + n + 1, cop);
memset(len, 0, sizeof(len));
for(int i = 1; i <= n; i ++) {//保證r遞增 ;
if(t1[i].l > maxl) {//若l遞增 (這些線段不可互相包含);
t2[++ cnt] = t1[i];//加入t2(用於DP) ;
maxl = t1[i].l;
}
else len[++ tot] = t1[i].r - t1[i].l;//r遞增l遞減,加入t1(說明這些線段有包含關係);
}
sort(len + 1, len + tot + 1, greater<int>());//長度從大到小排序 ;
for(int i = 2; i <= n; i ++) len[i] += len[i-1];//求包含其它線段的線段的長度字首和;
sort(t2 + 1, t2 + cnt + 1, cop);//這些線段不相互包含 ;
r = 1;//滾動陣列優化;
for(int i = 1; i <= cnt; i ++) {//保證r遞增(已經排序) (預處理操作);
if(t2[1].r <= t2[i].l) f[0][i] = -1e9;//1號線段與i號線段為空集 ;
else f[0][i] = t2[1].r - t2[i].l;//1號線段與i號線段有交集 ;
}
ans = max(ans, f[0][cnt] + len[k-1]);//1個集合 + (k - 1)個集合 ;
for(int j = 2; j <= min(k, cnt); j ++, r ^= 1) {//列舉選出j個集合,(j < k);
q[head = tail = 1] = 1; f[r][1] = -1e9;//滾動陣列清零 ;
for(int i = 2; i <= cnt; i ++) {
while(head <= tail&&t2[q[head]+1].r <= t2[i].l) head ++; //保證第j個集合對答案的貢獻為正;
if(head <= tail) f[r][i] = f[r^1][q[head]]+t2[q[head]+1].r-t2[i].l;
else f[r][i] = -1e9;
while(head <= tail&&f[r^1][i] + t2[i+1].r >= t2[q[tail]+1].r + f[r^1][q[tail]]) tail --;//維護f[x][j-1]+r[x+1]的最大值;
q[++ tail] = i;
}
ans = max(ans, f[r][cnt] + len[k - j]);//j個集合 + (k - j) 個集合
}
cout << ans << endl;
return 0;
}