从AT355-D引出来的笔记
用 3 种方法补 AT ABC 355-D 题时的感受
引入
上周的 AtCoder ABC 355 D 题
问题陈述
给你 \(N\) 个实数区间。 \(i\) - \((1 \leq i \leq N)\) 区间为 \([l_i, r_i]\) 。求使 \(i\) -th 和 \(j\) -th区间相交的 \((i, j)\,(1 \leq i \lt j \leq N)\) 对的个数。
说明
- \(2 \leq N \leq 5 \times 10^5\)
- \(0 \leq l_i \lt r_i \leq 10^9\)
- 所有输入值均为整数。
输入
输入内容由标准输入法提供,格式如下
\(N\)
\(l_1\) \(r_1\)
\(l_2\) \(r_2\)
\(\vdots\)
\(l_N\) \(r_N\)
输出
打印答案
输入样例 1
3
1 5
7 8
3 7
输出样例 1
2
给定的区间为 \([1,5], [7,8], [3,7]\) 。其中, \(1\) -st 与 \(3\) -rd 相交, \(2\) -nd 与 \(3\) -rd 相交,所以答案是 \(2\) 。
输入样例 2
3
3 4
2 5
1 6
输出样例 2
3
输入样例 3
2
1 2
3 4
输出样例 3
0
十年 OI 一场空,不开 long long 见祖宗
思路
其实时间复杂度都是 O(NlogN) 的,因为都要排序
思路一:题解思路,扫描
思路
最简单的方法就是题解写的 \(O(N)\) 的方法 sort:你好
将线段的左右端点拆开进行排序,然后再从第一个点开始往右扫直到扫完所有点为止。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+5;
#define int long long
struct node{
int w,t;
}a[N*2];
int n,ans,sum;
// sum 用来记录当前扫描的线上有多少线段
bool cmp(node a,node b) { return a.w == b.w ? a.t < b.t : a.w < b.w ; }
signed main(){
cin>>n;
int u,v,xb=0;
for(int i=1;i<=n;i++){
cin>>u>>v;
a[++xb]={u,0};
a[++xb]={v,1};
}
sort(a+1,a+1+xb,cmp);
for(int i=1;i<=xb;i++)
if(a[i].t==0)
ans+=sum++;
else
sum--;
cout<<ans;
return 0;
}
思路二:堆/优先队列
思路
还是先按照左端点进行排序,造一个小根堆,每次枚举完线段后将线段右端点添加到堆里,添加复杂度 \(O(logN)\),由于每个右端点只会被添加一次,这里的总复杂度是 \(O(NlogN)\)。
然后每次枚举线段后将堆中小于当前线段左端点的点弹出,因为既然这个端点已经比当前线段的左端点小了,再往后它也不会和其它线段重叠了,所以可以将它弹出。弹出复杂度 \(O(logN)\),由于每个右端点只会被弹出一次,这里的总复杂度是 \(O(NlogN)\)。
所以总复杂度还是 \(O(NlogN)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct ques{
int l,r;
};
struct node{
int n;
bool operator < (const node &a) const { return n > a.n ; }
};
int n;
vector <ques> g;
priority_queue <node> q;
bool cmp(ques a,ques b){ return a.l == b.l ? a.r < b.r : a.l < b.l ; }
signed main(){
cin>>n;
g.reserve(n);
for(int i=1;i<=n;i++)
g.push_back({read(),read()});
sort(g.begin(),g.end(),cmp);
int ans=0;
for(ques i:g){
while(!q.empty()&&q.top().n<i.l)
q.pop();
ans+=q.size();
q.push({i.r});
}
cout<<ans;
return 0;
}
思路三:树状数组
思路
还是先按照线段的左端点从小到大排序,然后依次枚举线段,每次枚举线段时将树状数组里存放的右端点大于等于这条线段的左端点的数量累加到答案里。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+5;
#define int long long
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct ques{
int l,r;
};
int n,mx;
int t[N*2];
vector <ques> g;
vector <int> un;
inline bool cmp(ques a,ques b){ return a.l==b.l ? a.r > b.r : a.l < b.l ; }
inline int lowbit(int x){ return x & (-x) ; }
inline void add(int x){
while(x<=mx){
t[x]++;
x+=lowbit(x);
}
}
inline int sum(int x){
int ans=0;
while(x>0){
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
signed main(){
cin>>n;
int l,r;
g.reserve(n);
un.reserve(n*2+5);
for(int i=1;i<=n;i++)
g.push_back({l=read(),r=read()}),
un.push_back(l),un.push_back(r);
sort(g.begin(),g.end(),cmp);
sort(un.begin(),un.end());
auto len=unique(un.begin(),un.end());
sort(un.begin(),len);
for(int i=0;i<g.size();i++)
g[i].l=lower_bound(un.begin(),len,g[i].l)-un.begin()+1,
g[i].r=lower_bound(un.begin(),len,g[i].r)-un.begin()+1,
mx=max(mx,g[i].r);
int ans=0,su=0;
for(ques i:g){
ans+=(su++)-sum(i.l-1);
add(i.r);
}
cout<<ans;
return 0;
}
笔记
1.双条件类型题
令线段 \(A\) 在线段 \(B\) 的前面,即线段 \(A\) 的左端点在线段 \(B\) 的左端点前面。那么,当线段 \(A\) 的右端点小于线段 \(B\) 的左端点时,我们就可以说线段 \(A\) 和线段 \(B\) 有重叠,也就是符合题目描述的一种情况。这时,为了方便解题,我们通常通过以线段左端点 \(l\) 为关键字从小到大排序的方式满足其中一重条件,再枚举线段右端点 \(r\),来构造满足题目要求的情况。
从这道题上我们可以看得出一类题:双条件类型题,即满足题目条件的情况有两重限制条件。这时候,我们通常用排序等方式使得我们枚举时天然满足其中一种条件,再通过一定手段满足另一种条件,以此枚举题目中的合法情况。解题时应分析清楚题目要求情景,挖掘出其中的隐藏条件。
2.树状数组的应用
树状数组本身作为一种支持快速修改的前缀和,常用的有两种:
1.单点修改,区间查询。
2.区间修改,单点查询。
而对于树状数组题目,题目如果不明说对于区间的改动,可以视为往数轴上放点,就像本题和 P5094 [USACO04OPEN] MooFest G 那样(这是加强版,原版 \(O(N^2)\))都能过。
P5094 [USACO04OPEN] MooFest G
思路
对于这道题,题目所求的内容限制有两条:
- 两头牛之间的说话声按照听力 \(v\) 取最大值
- 两头牛之间的距离是正数,所以带有绝对值
显然,用听力 \(v\) 为关键字排序更简单,然后开两个树状数组 \(tx\) 和 \(th\),分别表示对于目前枚举到的牛 \(i\),有多少牛的坐标在它前面,在它前面的牛的坐标和是多少。然后开个变量 \(he\),表示枚举到现在所有牛的坐标之和是多少。然后,每次更新的答案可以分两种情况算:
- \(x_i \lt x_j\)
- \(x_i \gt x_j\)
对于情况 1,答案应该加上 \((所有情况 1 的数量 - 所有情况 1 的坐标之和 ) * 当前牛的听力\),对于情况 2,答案应该加上 \((所有情况 2 的坐标之和 - 所有情况 2 的数量乘上当前牛的坐标) * 当前牛的听力\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+5;
#define int long long
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct cow{
int v,x;
};
class Tree{
private:
int t[N];
int lowbit(int x){return x & (-x) ; }
public:
void add(int x,int mx,int y){
while(x<=mx){
t[x]+=y;
x+=lowbit(x);
}
}
int sum(int x){
int reu=0;
while(x>0){
reu+=t[x];
x-=lowbit(x);
}
return reu;
}
void debug(int n){
cout<<endl<<endl;
for(int i=1;i<=n;i++)
cout<<t[i]<<" ";
cout<<endl<<endl;
}
}tx,th;
int n,mx;
vector <cow> g;
bool cmp(cow a,cow b){ return a.v == b.v ? a.x < b.x : a.v < b.v ; }
signed main(){
cin>>n;
g.reserve(n);
int v,x;
for(int i=1;i<=n;i++)
g.push_back({v=read(),x=read()});
sort(g.begin(),g.end(),cmp);
int ans=0;
int he=0;
int xb=0;
for(cow i:g){
int cnt=tx.sum(i.x),sum=th.sum(i.x);
ans+=(cnt*i.x-sum)*i.v;
ans+=((he-sum)-(xb-cnt)*i.x)*i.v;
tx.add(i.x,N,1),th.add(i.x,N,i.x);
he+=i.x;
xb++;
}
cout<<ans;
return 0;
}