以组为单位订音乐会的门票
代码来源
2024.9.28 每日一题 2286 [线段树] 很久没遇到了_哔哩哔哩_bilibili
题目
一个音乐会总共有 n
排座位,编号从 0
到 n - 1
,每一排有 m
个座椅,编号为 0
到 m - 1
。你需要设计一个买票系统,针对以下情况进行座位安排:
- 同一组的
k
位观众坐在 同一排座位,且座位连续 。 k
位观众中 每一位 都有座位坐,但他们 不一定 坐在一起。
由于观众非常挑剔,所以:
- 只有当一个组里所有成员座位的排数都 小于等于
maxRow
,这个组才能订座位。每一组的maxRow
可能 不同 。 - 如果有多排座位可以选择,优先选择 最小 的排数。如果同一排中有多个座位可以坐,优先选择号码 最小 的。
请你实现 BookMyShow
类:
BookMyShow(int n, int m)
,初始化对象,n
是排数,m
是每一排的座位数。int[] gather(int k, int maxRow)
返回长度为2
的数组,表示k
个成员中 第一个座位 的排数和座位编号,这k
位成员必须坐在 同一排座位,且座位连续 。换言之,返回最小可能的r
和c
满足第r
排中[c, c + k - 1]
的座位都是空的,且r <= maxRow
。如果 无法 安排座位,返回[]
。boolean scatter(int k, int maxRow)
如果组里所有k
个成员 不一定 要坐在一起的前提下,都能在第0
排到第maxRow
排之间找到座位,那么请返回true
。这种情况下,每个成员都优先找排数 最小 ,然后是座位编号最小的座位。如果不能安排所有k
个成员的座位,请返回false
。
示例
输入: ["BookMyShow", "gather", "gather", "scatter", "scatter"] [[2, 5], [4, 0], [2, 0], [5, 1], [5, 1]] 输出: [null, [0, 0], [], true, false] 解释: BookMyShow bms = new BookMyShow(2, 5); // 总共有 2 排,每排 5 个座位。 bms.gather(4, 0); // 返回 [0, 0] // 这一组安排第 0 排 [0, 3] 的座位。 bms.gather(2, 0); // 返回 [] // 第 0 排只剩下 1 个座位。 // 所以无法安排 2 个连续座位。 bms.scatter(5, 1); // 返回 True // 这一组安排第 0 排第 4 个座位和第 1 排 [0, 3] 的座位。 bms.scatter(5, 1); // 返回 False // 总共只剩下 1 个座位。
提示
1 <= n <= 5 * 104
1 <= m, k <= 109
0 <= maxRow <= n - 1
gather
和scatter
总 调用次数不超过5 * 104
次。
知识点
1、线段树、二分查找
2、c++动态数组vector的声明
vector<long long> sum;
sum.resize(4*n);
3、注意数据范围,此题有些地方要开Long Long
代码
class BookMyShow {
int n;
int m;
vector<long long> sum;
vector<int> used;
public:
BookMyShow(int n, int m) {
this->n=n;
this->m = m;
sum.resize(4*n);
used.resize(4*n);
}
vector<int> gather(int k, int maxRow) {
//根节点是闭区间,目标范围是半闭区间
int r = FindFirst(1,1,n,1,maxRow+1,m-k); //从根节点出发,下标从1到n;m-k表示该桶的最多使用量
if(r<1) return {};
int c=QuerySum(1,1,n,r,r);
Modify(1,1,n,r,k); //往第r个桶加入k个量
return {r-1,c};
}
bool scatter(int k, int maxRow) {
long s = QuerySum(1,1,n,1,maxRow+1);
if(1LL * (maxRow+1)*m - s < k) return false;
else{
for(int i=FindFirst(1,1,n,1,maxRow+1,m-1);k>0;i++){
int left = (int)m-QuerySum(1,1,n,i,i);
Modify(1,1,n,i,min(left,k));
k-=left;
}
return true;
}
}
void Modify(long long o, int l, int r, int idx, int val){
if(l==r){
sum[o]+=val;
used[o]+=val;
return;
}
//递归更新左右子树
int m=l+(r-l)/2;
if(idx<=m) Modify(o*2,l,m,idx,val);
else Modify(o*2+1,m+1,r,idx,val);
//向上更新父节点
used[o] = min(used[o*2],used[o*2+1]);
sum[o] = sum[o*2]+sum[o*2+1];
}
long long QuerySum(int o, int l, int r, int L,int R){
if(L<=l && R>=r) return sum[o];
long long res=0;
int m = l+(r-l)/2;
if(L<=m) res+=QuerySum(o*2,l,m,L,R);
if(m<R) res+= QuerySum(o*2+1,m+1,r,L,R);
return res;
}
// val是被使用的最多数量
// used[]维护被使用的最小值
int FindFirst(int o, int l, int r, int L, int R, int val){
if(used[o]>val) return 0;
if(l==r) return l;
int m = l+(r-l)/2;
if(used[o*2]<=val) return FindFirst(o*2,l,m,L,R,val);
if(m<R) return FindFirst(o*2+1,m+1,r,L,R,val); //要在查询区间内
return 0;
}
};
/**
* Your BookMyShow object will be instantiated and called as such:
* BookMyShow* obj = new BookMyShow(n, m);
* vector<int> param_1 = obj->gather(k,maxRow);
* bool param_2 = obj->scatter(k,maxRow);
*/
/**
区间标记和区间查询——线段树
区间修改——>单点修改——因为每排占位是联系的
*/
反思
还没有完全弄懂,但有以下收获
线段树的基本原理
1、线段树是一个二叉树,每个节点存储的是某个区间的统计值(比如区间和)
2、叶子节点存储的是数组中的某个具体元素
3、内部节点存储的是其左右子树的区间和,即区间[l,r]的和。根节点存储整个数组的和。
关于代码中的函数
1、关于FindFirst函数为什么最后return l的条件是(l==r),当l==r时,代表找到了叶子节点(即某个具体的排)满足题目条件。
这种递归查找模式常用于线段树的区间查询或定位操作,保证找到的是最符合条件的具体位置。
2、QuerySum函数是一个在线段树中进行区间求和的函数,利用了分治的思想来查询某个区间[L,R]内的元素和。
o表示当前线段树节点的编号,这里假设从1开始
[l,r]表示节点o管理的区间范围
[L,R]表示我们要查询的区间范围
分治思想
若当前节点管理的区间和查询的区间部分重叠,我们需要将当前区间划分为左右两个子区间,递归查询左右子区间的和。
3、为什么modify需要递归。
线段树的每个节点的值都依赖于它的左右子节点的值,因此每次修改一个叶子节点的值,必须递归更新沿途的所有节点,确保整个线段树中的区间和保持正确。