传送门
https://leetcode-cn.com/problems/t3fKg1/
题目大意
给出n条线段
(
l
,
r
,
n
e
d
)
(l,r,ned)
(l,r,ned),表示从
l
l
l到
r
r
r,需要包含
n
e
d
ned
ned个点,要求你在数轴上选最少的整数点(一个点只能选一次),使得所有线段的需求都被满足。保证有解。
l
,
r
≤
1
0
9
,
n
≤
1
0
5
l,r\leq 10^9, n\leq 10^5
l,r≤109,n≤105
题解
Step 1:和经典的贪心问题比较类似,很容易想出一个贪心做法,先把线段按右端点排序,然后依次处理线段的需求,如果已经选了足够的点,就不操作,否则尽量选靠右的点。因为这样能在满足当前线段需求的前提下,最大化满足后面线段的需求,显然这个策略是正确的。
Step 2:直接模拟,复杂度 O ( n ∗ max ( r ) ) O(n*\max(r)) O(n∗max(r))爆炸无疑,容易看出可以加上离散化,优化到 O ( n 2 ) O(n^2) O(n2)
Step 3:“尽量选靠右的点”,在朴素做法中需要枚举才能找到可选择的点(注意,为什么不是直接选靠右的若干个点?因为这些点之前也许被选过),在这里花费了
O
(
n
)
O(n)
O(n)的时间,如何优化?用一个链表即可,使用
n
e
x
t
(
i
)
next(i)
next(i)表示
i
i
i左边第一个没有被选择的位置。这样能
O
(
1
)
O(1)
O(1)找到可用的点(比如,第一个可用点是
n
e
x
t
(
r
)
next(r)
next(r))。考虑维护它,当一个点被选择以后,将其在链表里删除即可,方法很多。不过需要注意,坐标之前经过了离散化,所以一个节点代表了之前的一段区间,只有当该区间的点全部被选择之后才能将该节点删除(具体如何操作?我们并不关心哪些点被选了,直接用一个变量记录该区间被选择了多少或者剩下多少即可)这部分总复杂度变为
O
(
n
)
O(n)
O(n)
唯一剩下的问题是如何计算“是否满足当前线段需求”,是一个简单的区间和问题,使用具有单点加和区间求和的数据结构处理即可,我选择了树状数组,因为好写,常数小。这部分复杂度为
O
(
n
log
n
)
O(n\log n)
O(nlogn)
题外话
我在场上只想到step 2.5,然后就在线段树上去做了,当时想的是通过不断往下分裂的方式凑够需要的点数,即只做一次update操作,而不是不断选点不断update,没写完。后来想一想,线段树的时间复杂度是对的,只不过就是非常难写,因为这题本身带了离散化,就很容易出错,况且常数也更大,当时着急了,怕写不完,没仔细想。
这个题算是组装题,大概三部分,离散化+贪心+链表,链表这个技巧最初应该是在某个染色的题目上遇见的,难度不算很大,场上调T4调太久了,没剩多少时间,虽然补题我也花了很多时间hhh
代码
class Solution {
#define maxn 100010
class Task{
public:
int l,r,ned;
Task(int l=0,int r=0,int ned=0):l(l),r(r),ned(ned){}
bool operator < (const Task &rhs)const{
return r<rhs.r;
}
}t[maxn];
int x[maxn*2];
int filled[maxn*2];//filled[i]表示第i块已经填充了多少
//某个位置大小
int size(int pos){return x[pos+1]-x[pos];}
int nxt[maxn*2];
//树状数组用于求和
class Bit
{
int C[maxn*2];
int n;
public:
Bit(int _n):n(_n){memset(C,0,sizeof C);}
#define lowbit(x) ((x)&(-(x)))
void update(int pos,int v){
for(int i=pos;i<=n;i+=lowbit(i)) C[i]+=v;
}
int query(int pos){
int ret=0;
for(int i=pos;i;i-=lowbit(i)) ret+=C[i];
return ret;
}
int query(int l,int r){return query(r)-query(l-1);}
};
public:
Solution(){
memset(x,0,sizeof x);
memset(filled,0,sizeof filled);
memset(nxt,0,sizeof nxt);
}
int processTasks(vector<vector<int>>& tasks) {
int n=tasks.size();
for(int i=0;i<n;i++){
t[i+1]=Task(tasks[i][0],tasks[i][1]+1,tasks[i][2]);
x[i*2+1]=t[i+1].l;
x[i*2+2]=t[i+1].r;
}
//离散化
sort(x+1,x+n*2+1);
int xN=unique(x+1,x+n*2+1)-x-1;
for(int i=1;i<=n;++i){
t[i].l=lower_bound(x+1,x+xN+1,t[i].l)-x;
t[i].r=lower_bound(x+1,x+xN+1,t[i].r)-x;
}
//链表
for(int i=1;i<=xN+1;++i) nxt[i]=i-1;
Bit bit(xN);
sort(t+1,t+n+1);
for(int i=1;i<=n;++i)
{
//已经开机的时间
int got=bit.query(t[i].l,t[i].r-1);
if(got<t[i].ned)
{
//还需要补足的时间
int ned = t[i].ned-got;
for(int j=nxt[t[i].r];;j=nxt[j])
{
int left = size(j)-filled[j];//还可以用于填充的部分
//整个填充
if(left<ned){
bit.update(j,left);
//填充满,标记上,下次使用下一个块
filled[j]+=left;
nxt[t[i].r]=nxt[j];
ned-=left;
}
//部分填充 left>=ned
else{
bit.update(j,ned);
filled[j]+=ned;
//该块填充满
if(filled[j]==size(j)) nxt[t[i].r]=nxt[j];
break;
}
}
}
}
return bit.query(xN);
}
};