区间相关
求区间交和并
给定一些区间,求出这些区间的交集和并集。
-
交集一定连续。我们维护左端点和右端点,按照任意顺序扫描并维护即可。
-
并集,先按照左端点排序,然后扫过去,维护最大右端点,出现下一个左端点大于上一个右端点的时候,砍掉这个区间。
Pjudge NOIP Round #4
【题意】
虱子国王尼特这天有点不舒服,它周围的 \(n\) 个医生立刻开出了药方:第 \(i\) 个医生告诉它,从这天起的第 \(L_i\) 天到第 \(R_i\) 天,它应该服用 \(x_{i,1},x_{i,2},…,x_{i,K_i}\) 这 \(K_i\) 种药,每天每种药应当服用恰好一片。注意,如果有多个医生的药方里都要求尼特在第 \(p\) 天服用第 \(q\) 种药,那尼特在第 \(p\) 天仍然只会服用一片第 \(q\) 种药。编号为 \(j\) 的药每片需要 \(c_j\) 元钱。
然而,由于尼特的疏忽,有恰好一位庸医混进了医生队伍里,但尼特并不知道哪位医生是庸医。所以它想知道,对于所有 \(1≤i≤n\),如果它按照除了第 \(i\) 个医生之外的所有医生的药方吃药,它总共将花费多少钱。
\(n,m \le 5\times 10^5,\sum K_i \le 10^6, c_i \le 10^6\)
【分析】
按照上次那道区间颜色的套路,我们首先求出所有医生的话都要听的时候的答案,然后对于每个医生考虑消除贡献。
“首先”这一步怎么做?对每一个颜色维护区间并,然后乘以权值即可。这部分代码:
f(i,1,m) {
int sum = 0, l = 0, r = 0;
sort(y[i].begin(),y[i].end(),cmp);
for(pair<pii,int> j : y[i]) {
if(j.fi.fi <= r) r = max(j.fi.se,r);
else {sum+=(r==0?0:dis[r]-dis[l]); l=j.fi.fi,r=j.fi.se; }
}
sum+=(dis[r]-dis[l]);
pans+=sum*c[i];
}
然后考虑维护一个颜色里有哪些是只有一个医生覆盖的区间。考虑扫描线并且维护一个 set,表示当前扫到的点里面有哪些医生。如果只有一个医生,那么整个区间都应该算贡献。这要怎么做比较好写?如果直接用 \(l\) 和 \(r\) 处理,会遇到一些边界情况。其实考虑 \(r + 1\) 时刻,set 中才会真正删去一个点。这样把 \(r\) 加 \(1\) 再处理会更好。
时间复杂度 \(O(n \log n)\)。这次还真过不去。
考虑卡常。把 set
换成 gp_hash_table
。然后构造函数记得算时间复杂度,不要退化了。
然后过了。
#include<bits/stdc++.h>
#include<ext/pb_ds/tree_policy.hpp>
#include<ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int dis[1000010], c[1000010], ans[1000010];
int n,m; int v;
struct doc{
int l,r,k;
vector<int> p;
}d[1000010];
int enc(int x) {return lower_bound(dis+1,dis+v+1,x)-dis;}
#define fi first
#define se second
bool cmp(pair<pii,int> x,pair<pii,int> y){
if(x.fi.fi!=y.fi.fi)return (x.fi.fi<y.fi.fi);
return (x.fi.se<y.fi.se);
}
vector<pair<pii,int>>y[1000010];
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
//think twice,code once.
//think once,debug forever.
cin>>n>>m;
f(i,1,m)cin>>c[i];
int cnt=0;
f(i,1,n){
cin>>d[i].l>>d[i].r>>d[i].k;
d[i].r++;
dis[++cnt]=d[i].l; dis[++cnt]=d[i].r;
f(j,1,d[i].k) { int x; cin>>x; d[i].p.push_back(x); }
}
sort(dis+1,dis+cnt+1); v=unique(dis+1,dis+cnt+1)-dis-1;
int pans = 0;
f(i,1,n) for(int j : d[i].p) y[j].push_back({{enc(d[i].l),enc(d[i].r)},i});
f(i,1,m) {
int sum = 0, l = 0, r = 0;
sort(y[i].begin(),y[i].end(),cmp);
for(pair<pii,int> j : y[i]) {
if(j.fi.fi <= r) r = max(j.fi.se,r);
else {sum+=(r==0?0:dis[r]-dis[l]); l=j.fi.fi,r=j.fi.se; }
}
sum+=(dis[r]-dis[l]);
pans+=sum*c[i];
}
f(i,1,n)ans[i]=pans;
int ccc = 0;
gp_hash_table<int, null_type> stk;
vector<vector<int>> jin(cnt+1); vector<vector<int>> chu(cnt+1);vector<int> vis;
f(i,1,m){
stk.clear(); vis.clear();
for(pair<pii,int> j : y[i]) {
vis.push_back(j.fi.fi); vis.push_back(j.fi.se);
}
for(int i : vis) {
jin[i].clear(); chu[i].clear();
}
for(pair<pii,int> j : y[i]) {
jin[j.fi.fi].push_back(j.se); chu[j.fi.se].push_back(j.se);
}
sort(vis.begin(),vis.end());
int ccnt = unique(vis.begin(),vis.end())-vis.begin()-1;
int lst = 0;
f(jj,0,ccnt){
ccc++;
int j=vis[jj];
if(!chu[j].size()&&!jin[j].size())continue;
if(stk.size() == 1) {
int dx = (*(stk.begin())); ans[dx]-=c[i]*max(0ll, dis[j]-1-lst + 1);
}
for(int k : jin[j]) { stk.insert(k); if(stk.size() == 1) lst = dis[j]; }
for(int k : chu[j]) { stk.erase(k); if(stk.size() == 1) lst = dis[j]; }
}
}
f(i,1,n)cout<<ans[i]<<" "; cout <<endl;
time_t finish = clock();
// cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
区间定序贡献
为什么要对区间进行“左端点排序”/“右端点排序”/按照xxx顺序排序?
实际上是因为,这样排序能够对条件进行简化,以满足题目要求。如果从左往右扫,左端点排序能够让之前选到的任何区间的子集交/并的不会出现左端点大于当前区间左端点的情况。右端点排序能够让当前选到的区间的子集交/并的右端不会超出当前右端点。这都是简化区间之间关系的一种方式,和序列排序降维度的思想类似。
这里看一道题:
【题意】
给定若干个端点 \(a_i\),对每一个端点有一个长度 \(\ell_i\)。现在要给每一个端点定一个向左或者向右的方向,将其变成一条长度为 \(\ell_i\) 的线段。要求让线段的并长度尽可能大,输出长度。
【分析】
这里我们首先考虑的是新加入一个区间能够有什么效果。效果是,能够覆盖之前是空的的一些位置,对长度造成贡献。这个贡献,我们假设之前的区间右端点为 \(r\),那么贡献是右边这一段加上前面可能有的若干个空档。这个东西看着难以维护,但是如果有空档说明中间的一些区间是完全被包含的,也就是说它们实际上不产生任何贡献!于是,我们只需要计算右边那一段(得到的贡献不会更优),并且并不选择所有区间,而是跳过一些区间(使得右端点正常)。
这时候我们要注意一个问题,不能让某个新加入的区间右边有一个和它并没有交集的区间,否则就会出问题。这样的话我们容易考虑到这时候按照左/右端点排序都是不可以的,我们需要按照原先的中间端点排序,才能达到这个目的。