shr专题
2024.11.3 平衡树
前言:又是一个周日。又是一个shr学长来的周日……
好困好困,果然不应该一上午都玩原神的……
“常用编译命令”
~/work$ g++ a+b.cpp -o a+b.exe 编译
-O2 -statid 吸氧优化
-std=c++14 c++14编译
-Wall 可以检测出很多细节问题 比如变量没有初始化、没调用等 缺点:啥玩意儿?freopen?
-ulimit -s unlimited 栈空间?
-ulimit -s 54288
“测试??命令”
time ./a+b.exe 时间
diff out.out ans.ans 比对两个文件是否相同
FHQ-Treap
不带旋的平衡树,代码简洁简单易懂,好评!
1.1 板子
FHQ-Treap 只需要分裂和合并操作就可以完成各种查询。其中,分裂有按排名分裂与按值分裂,按值分裂是将原本的树分裂成两棵根分别为
按排名分裂的平衡树中序遍历是原数组,按值分裂的平衡树中序遍历是有序的元素。所以一般来说,无法同时按排名分裂、按值分裂?
嘻嘻
板
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n;
int rt,cnt;
struct fhq{
int ls,rs;
int x,pri;
int siz;
}tr[N];
void push_up(int id) { tr[id].siz=tr[tr[id].ls].siz+tr[tr[id].rs].siz+1; }
int newnode(int x)
{
cnt++;
tr[cnt].ls=tr[cnt].rs=0;
tr[cnt].x=x,tr[cnt].pri=rand();
tr[cnt].siz=1;
return cnt;
}
void split(int id,int x,int &l,int &r)
{
if (!id) { l=r=0; return ; }
if (tr[id].x<=x) { l=id; split(tr[id].rs,x,tr[id].rs,r); }
else { r=id; split(tr[id].ls,x,l,tr[id].ls); }
push_up(id);
}
int merge(int l,int r)
{
if (!l||!r) return l+r;
if (tr[l].pri>tr[r].pri)
{
tr[l].rs=merge(tr[l].rs,r);
push_up(l);
return l;
}
else
{
tr[r].ls=merge(l,tr[r].ls);
push_up(r);
return r;
}
}
void _insert(int x)
{
int l,r;
split(rt,x,l,r);
rt=merge(merge(l,newnode(x)),r);
}
void _delete(int x)
{
int l,p,r;
split(rt,x,l,r),split(l,x-1,l,p);
rt=merge(merge(l,merge(tr[p].ls,tr[p].rs)),r);
}
int query1(int x)
{
int l,r;
split(rt,x-1,l,r);
int res=tr[l].siz+1;
rt=merge(l,r);
return res;
}
int query2(int id,int x)
{
if (tr[tr[id].ls].siz+1==x) return id;
if (tr[tr[id].ls].siz+1<x) return query2(tr[id].rs,x-tr[tr[id].ls].siz-1);
else return query2(tr[id].ls,x);
}
int pre(int x)
{
int l,r;
split(rt,x-1,l,r);
int res=tr[query2(l,tr[l].siz)].x;
rt=merge(l,r);
return res;
}
int ea(int x)
{
int l,r;
split(rt,x,l,r);
int res=tr[query2(r,1)].x;
rt=merge(l,r);
return res;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1,op,x;i<=n;i++)
{
cin>>op>>x;
if (op==1) _insert(x);//插入元素x
else if (op==2) _delete(x);//删除一个元素x
else if (op==3) cout<<query1(x)<<"\n";//查询元素x的排名
else if (op==4) cout<<tr[query2(rt,x)].x<<"\n";//查询第x大的元素
else if (op==5) cout<<pre(x)<<"\n";//查询x的前驱
else cout<<ea(x)<<"\n";//查询x的后继
}
return 0;
}
1.2 文艺平衡树(区间翻转)
有个结论:对于一棵平衡树,交换所有节点的左右子树后的中序遍历相当于是翻转区间后的序列。手模易得,不会证
所以,给每个节点打上一个
板*2
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m,l,r;
int cnt,root;
struct node{
int ls,rs,siz,tag;
int num,pri;
}tr[N];
void push_down(int id)
{
if (!tr[id].tag) return ;
swap(tr[id].ls,tr[id].rs);
tr[tr[id].ls].tag^=1,tr[tr[id].rs].tag^=1;
tr[id].tag=0;
}
void push_up(int id) { tr[id].siz=tr[tr[id].ls].siz+tr[tr[id].rs].siz+1; }
int newnode(int x)
{
cnt++;
tr[cnt].ls=tr[cnt].rs=tr[cnt].tag=0;
tr[cnt].siz=1,tr[cnt].num=x,tr[cnt].pri=rand();
return cnt;
}
void split(int id,int x,int &l,int &r)
{
if (!id) { l=r=0; return ; }
push_down(id);
if (tr[tr[id].ls].siz+1<=x) { l=id; split(tr[id].rs,x-tr[tr[id].ls].siz-1,tr[id].rs,r); }
else { r=id; split(tr[id].ls,x,l,tr[id].ls); }
push_up(id);
}
int merge(int l,int r)
{
if (!l||!r) return l+r;
if (tr[l].pri>tr[r].pri) { push_down(l); tr[l].rs=merge(tr[l].rs,r); push_up(l); return l; }
else { push_down(r); tr[r].ls=merge(l,tr[r].ls); push_up(r); return r; }
}
void out(int id)
{
if (!id) return ;
push_down(id);
out(tr[id].ls),cout<<tr[id].num<<" ",out(tr[id].rs);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++) root=merge(root,newnode(i));
while (m--)
{
cin>>l>>r;
int lrt,rrt,p;
split(root,r,lrt,rrt);//l:[1,r]
split(lrt,l-1,lrt,p);
tr[p].tag^=1;
root=merge(merge(lrt,p),rrt);
}
out(root);
return 0;
}
2.1 还是区间翻转
你说得对,但是这题第一眼看过去感觉就是要同时权值分裂、排名分裂
然后题解区出来了一个无比聪明的想法:按排名建树,将普通平衡树的
当然了,这是肯定能被
警示自己:先下传标记再返回!!!
时刻牢记 Treap 既有 BST 的性质也有堆的性质……
呃啊
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,a[N];
int rt,cnt;
struct node { int x,id; }p[N];
struct fhq{
int ls,rs;
int x,pri;
int siz,tag;
}tr[N];
bool cmp(node a,node b)
{
if (a.x!=b.x) return a.x<b.x;
return a.id<b.id;
}
int newnode(int x,int pr)
{
cnt++;
tr[cnt]={0,0,x,pr,1,0};
return cnt;
}
void push_up(int id) { tr[id].siz=tr[tr[id].ls].siz+tr[tr[id].rs].siz+1; }
void push_down(int id)
{
if (!tr[id].tag) return ;
swap(tr[id].ls,tr[id].rs);
tr[tr[id].ls].tag^=1,tr[tr[id].rs].tag^=1;
tr[id].tag=0;
}
int merge(int l,int r)
{
push_down(l),push_down(r);
if (!l||!r) return l+r;
if (tr[l].pri<tr[r].pri) { tr[l].rs=merge(tr[l].rs,r); push_up(l); return l; }
else { tr[r].ls=merge(l,tr[r].ls); push_up(r); return r; }
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1;i<=n;i++) { cin>>p[i].x; p[i].id=i; }
sort(p+1,p+1+n,cmp);
for (int i=1;i<=n;i++) a[p[i].id]=i;
for (int i=1;i<=n;i++) rt=merge(rt,newnode(i,a[i]));
for (int i=1;i<=n;i++)
{
cout<<tr[tr[rt].ls].siz+i<<" ";
tr[tr[rt].ls].tag^=1;
rt=merge(tr[rt].ls,tr[rt].rs);
}
return 0;
}
2024.11.24-二维数点
没错这又是shr学长来的一天。
上次讲了平衡树,这次讲了二维数点。
二维数点
二维数点,就是在二维平面上数点的个数
一般是把一维在线操作转化为二维离线的操作,查询的时间就是新的一维(也有其他的转化)
呃啊
比如,给出一个序列
1.1 板子
这里就是将下标
联想到前缀和,要求这个矩阵的点数,相当于是四个四分之一平面的答案经过一番加减之后的结果。
(这题中更简单,直接用两个查询相减即可,也就是将一个询问拆成俩:
道理我都懂,但我还是不知道怎么实现啊
引入一个我不会的扫描线
之前一直听扫描线啥的,这啊那啊的,我现在终于知道是咋扫的了www
扫描线:每次根据一个维度进行有序扫描,扫描的过程中进行操作。
在这道题中就是扫描下标,每次有两种操作:加点,和查询。加点就是把
然后就有了
ans[q[i][j].id]+=_sum(q[i][j].x)*q[i][j].v;
然后就有了
二维数点板子代码(?)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5;
int n,m;
int a[N];
struct node{
int x,id,v;
};
vector <node> q[N];
int tr[N];
int ans[N];
int lowbit (int x) { return x&-x; }
void _update(int d,int x)
{
while (d<N) tr[d]+=x,d+=lowbit(d);
}
int _sum(int x)//查询
{
int res=0;
while (x) res+=tr[x],x-=lowbit(x);
return res;
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
for (int i=1,l,r,x;i<=m;i++)
{
scanf("%lld%lld%lld",&l,&r,&x);
q[l-1].push_back((node){x,i,-1}),q[r].push_back((node){x,i,1});
}
for (int i=1;i<=n;i++)
{
_update(a[i],1);
int _size=q[i].size();
for (int j=0;j<_size;j++) ans[q[i][j].id]+=_sum(q[i][j].x)*q[i][j].v;
}
for (int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
呃啊
1.2 小卡与落叶
这题也可以二维数点做。
每次查询如下图↓
刚开始跑一个dfs记录
对于
每次按照
基本上一遍过的代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m;
vector <int> tr[N];
struct node { int id,dfn,v; };
vector <node> q[N];
vector <int> add[N];
int tme;
int mx,cnt;
int t[N];
int ans[N];
int dfn[N],dep[N],siz[N];//dfs序,深度,子树大小
void dfs(int x,int _fa,int _dep)
{
mx=max(mx,_dep);
dfn[x]=++tme,dep[x]=_dep,siz[x]=1;
add[_dep].push_back(dfn[x]);
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==_fa) continue;
dfs(v,x,_dep+1);
siz[x]+=siz[v];
}
}
int lowbit(int x) { return x&-x; }
void _update(int d,int x)
{
while (d<N) t[d]+=x,d+=lowbit(d);
}
int _sum(int x)
{
int res=0;
while (x) res+=t[x],x-=lowbit(x);
return res;
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,u,v;i<n;i++)
{
scanf("%lld%lld",&u,&v);
tr[u].push_back(v);
tr[v].push_back(u);
}
dfs(1,0,1);
for (int i=1,op,x,pre;i<=m;i++)
{
scanf("%lld%lld",&op,&x);
if (op==1) pre=x;
if (op==2)
{
q[pre].push_back({++cnt,dfn[x]+siz[x]-1,1});
q[pre].push_back({cnt,dfn[x]-1,-1});
}
}
for (int i=mx;i>=1;i--)
{
int _size1=add[i].size();
for (int j=0;j<_size1;j++) _update(add[i][j],1);
int _size2=q[i].size();
for (int j=0;j<_size2;j++) ans[q[i][j].id]+=_sum(q[i][j].dfn)*q[i][j].v;
}
for (int i=1;i<=cnt;i++) printf("%lld\n",ans[i]);
return 0;
}
1.3 HH的项链
每个区间相同的元素贡献一次就可以。发现第一次在区间出现的元素
嘻
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,a[N],m;
int lst[N],pre[N];
struct node { int ea,val,id; };
vector <node> q[N];
int tr[N],ans[N];
int lowbit(int x) { return x&-x; }
void add(int x,int y) { while (x<N) { tr[x]+=y; x+=lowbit(x); } }
int sum(int x)
{
int res=0;
while (x) { res+=tr[x]; x-=lowbit(x); }
return res;
}
void calc()
{
for (int i=1;i<=n;i++)
{
add(pre[i],1);
int _size=q[i].size();
for (int j=0;j<_size;j++) ans[q[i][j].id]+=sum(q[i][j].ea)*q[i][j].val;
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1;i<=n;i++)
{
cin>>a[i];
pre[i]=lst[a[i]];
if (!pre[i]) pre[i]=1;
lst[a[i]]=i+1;
}
cin>>m;
for (int i=1,l,r;i<=m;i++)
{
cin>>l>>r;
q[l-1].push_back({l,-1,i}),q[r].push_back({l,1,i});
}
calc();
for (int i=1;i<=m;i++) cout<<ans[i]<<"\n";
return 0;
}
1.4 扫描线:矩形面积并
居然不是二维数点,我还想半天……
首先 显然 ,根据出入边把混合图形掰成若干个矩形然后计算。用线段树维护当前横截面的长度,最后的答案就是每个横截面长度乘横坐标差值的和。
因为每个横截面只需要计算一次,即多加了也计算一次,所以线段树中记一个
码
#include <bits/stdc++.h>
#define int long long
#define y1 y_1
using namespace std;
const int N=5e5+5;
int n,cnt,ans;
int m;
struct node { int x,y1,y2,val; }e[N<<1];
int a[N<<1],idx[N<<1];
int mx;
struct Segment_Tree
{
struct Node{
int l,r;
int tag,len;
}tr[N<<2];
void push_up(int id)
{
if (tr[id].l==mx&&tr[id].r==mx) return ;
if (tr[id].tag) tr[id].len=idx[tr[id].r+1]-idx[tr[id].l];
else tr[id].len=tr[id<<1].len+tr[id<<1|1].len;
}
void build(int id,int l,int r)
{
tr[id].l=l,tr[id].r=r;
if (l==r) return ;
int mid=(l+r)>>1;
build(id<<1,l,mid),build(id<<1|1,mid+1,r);
}
void update(int id,int l,int r,int w)
{
if (tr[id].l>=l&&tr[id].r<=r)
{
tr[id].tag+=w;
push_up(id);
return ;
}
int mid=(tr[id].l+tr[id].r)>>1;
if (mid>=l) update(id<<1,l,r,w);
if (mid+1<=r) update(id<<1|1,l,r,w);
push_up(id);
}
}Tr;
void init()//离散化
{
sort(a+1,a+1+m);
int ea=unique(a+1,a+1+m)-a-1;
for (int i=1;i<=m;i++)
{
int pos1=lower_bound(a+1,a+ea+1,e[i].y1)-a;
int pos2=lower_bound(a+1,a+ea+1,e[i].y2)-a;
idx[pos1]=e[i].y1,idx[pos2]=e[i].y2;
e[i].y1=pos1,e[i].y2=pos2;
mx=max(mx,pos2);
}
}
bool cmp(node ea,node eaa) { return (ea.x!=eaa.x?ea.x<eaa.x:ea.val>eaa.val); }
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1,x1,y1,x2,y2;i<=n;i++)
{
cin>>x1>>y1>>x2>>y2;
e[++cnt]={x1,y1,y2,1};
e[++cnt]={x2,y1,y2,-1};
a[++m]=y1,a[++m]=y2;
}
init();
sort(e+1,e+1+cnt,cmp);
Tr.build(1,1,cnt);
for (int i=1;i<=cnt;i++)
{
Tr.update(1,e[i].y1,e[i].y2-1,e[i].val);
ans+=Tr.tr[1].len*(e[i+1].x-e[i].x);
}
cout<<ans;
return 0;
}
2024.12.8 三维数点
呃呃呃摆烂一上午,下午继续来听shr的专题
?
三维数点?
?
把一个立方体拆成8个(八分之一空间),常数较大
去看了会儿并查集,结果感觉漏掉了挺重要的事情……?优化常数的?
对于
超好理解的
做法有CDQ分治,树套树,KDT
k维数点可以KDT或CDQ套CDQ套CDQ……
CDQ分治
解决多为限制问题
在值域上不断 分治。对于一维,先噶出一个
小剪枝:对于一个区间,没有任何操作就return掉
然后就固定了一维,就变成了二维数点
三维数点模板
好困好困,难以思考。
用 CDQ 求出
先把所有输入排个序,然后直接 CDQ 。对于每个操作区间
剩下的“二维数点”其实不用完全按扫描线那样做。可以再分别给两个操作区间按第二关键字排序,两个区间有序后双指针统计。
因为会对每个操作区间再排个序,排序后两个区间的第一关键字相对有序,但内部顺序被打乱,所以要先处理小区间再处理该区间
另外,注意回溯,以及答案要存结构体里!
板
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,k,m;
struct node { int a,b,c,cnt,ans; }a[N],q[N];
int ans[N],cnt[N];
int tr[N];
bool cmp(node x,node y)
{
if (x.a!=y.a) return x.a<y.a;
if (x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}
bool cmp2(node x,node y)
{
if (x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}
int lowbit(int x) { return x&-x; }
void add(int x,int y) { while (x<N) { tr[x]+=y; x+=lowbit(x); } }
int sum(int x)
{
int res=0;
while (x) { res+=tr[x]; x-=lowbit(x); }
return res;
}
void cdq(int l,int r)
{
if (l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
sort(q+l,q+mid+1,cmp2),sort(q+mid+1,q+r+1,cmp2);
int j=l;
for (int i=mid+1;i<=r;i++)
{
while (q[i].b>=q[j].b&&j<=mid) { add(q[j].c,q[j].cnt); j++; }
q[i].ans+=sum(q[i].c);
}
for (int i=l;i<j;i++) add(q[i].c,-q[i].cnt);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for (int i=1;i<=n;i++) cin>>a[i].a>>a[i].b>>a[i].c;
sort(a+1,a+1+n,cmp);
int sum=1;
for (int i=1;i<=n;i++)
{
if (a[i].a!=a[i+1].a||a[i].b!=a[i+1].b||a[i].c!=a[i+1].c)
{
q[++m]=a[i],q[m].cnt=sum;
sum=0;
}
sum++;
}
cdq(1,m);
for (int i=1;i<=n;i++) cnt[q[i].ans+q[i].cnt-1]+=q[i].cnt;
for (int i=0;i<n;i++) cout<<cnt[i]<<"\n";
return 0;
}
例题1.1
很简,按照时间分治就行了。
但是在“前缀和”时,
以及,注意各种手误就好了
码
#include <bits/stdc++.h>
#define int long long
#define y1 y_1
using namespace std;
const int N=2e6+5;
const int M=2e5;
int n;
int op,x1,y1,x2,y2,k,tme,cnt;
struct node { int op,x,y,val,id; }q[M];
int tr[N],ans[N];
bool cmp(node ea,node eaa) { return (ea.x!=eaa.x?ea.x<eaa.x:ea.y<eaa.y); }
int lowbit(int x) { return x&-x; }
void add(int x,int y) { while (x<=n) { tr[x]+=y; x+=lowbit(x); } }
int sum(int x)
{
int res=0;
while (x) { res+=tr[x]; x-=lowbit(x); }
return res;
}
void cdq(int l,int r)
{
if (l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
sort(q+l,q+mid+1,cmp),sort(q+mid+1,q+r+1,cmp);
int j=l;
for (int i=mid+1;i<=r;i++)
{
while (j<=mid&&q[j].x<=q[i].x)
{
if (q[j].op==1) add(q[j].y,q[j].val);
j++;
}
if (q[i].op==2) ans[q[i].id]+=q[i].val*sum(q[i].y);
}
for (int i=l;i<j;i++) if (q[i].op==1) add(q[i].y,-q[i].val);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
while (cin>>op)
{
if (!op) cin>>n,n++;
else if (op==1) { cin>>x1>>y1>>k; q[++tme]={1,++x1,++y1,k,0}; }
else if (op==2)
{
cin>>x1>>y1>>x2>>y2;
x1++,y1++,x2++,y2++;
q[++tme]={2,x1-1,y1-1,1,++cnt};
q[++tme]={2,x2,y1-1,-1,cnt};
q[++tme]={2,x1-1,y2,-1,cnt};
q[++tme]={2,x2,y2,1,cnt};
}
else break;
}
cdq(1,tme);
for (int i=1;i<=cnt;i++) cout<<ans[i]<<"\n";
return 0;
}
例题1.2 逆序对喵
你说得对,但不是所有删点都需要逆向变加点的
你说得对,但这种形似“每次插入都需要查询所有”可以只计算这次插入对答案产生的新的贡献,最后前缀和一下就好了,不用一次插入后跟着
你说得对,但插入有时候确实能把查询的事儿也干了,模板不也是这样做的吗……
我好菜
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,m,tme;
int a[N],b[N],pos[N],f[N];
struct node { int x,y,id,val; }q[N];
int tr[N],ans[N];
bool cmp(node x,node y) { return x.x<y.x; }
int lowbit(int x) { return x&-x; }
void add(int x,int y)
{
while (x<=n) { tr[x]+=y; x+=lowbit(x); }
}
int sum(int x)
{
int res=0;
while (x) { res+=tr[x]; x-=lowbit(x); }
return res;
}
void cdq(int l,int r)
{
if (l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
sort(q+l,q+mid+1,cmp),sort(q+mid+1,q+r+1,cmp);
int j=l;
for (int i=mid+1;i<=r;i++)
{
while (j<=mid&&q[j].x<q[i].x) { add(q[j].y,q[j].val); j++; }
ans[q[i].id]+=q[i].val*(sum(n)-sum(q[i].y));
}
for (int i=l;i<j;i++) add(q[i].y,-q[i].val);
j=mid;
for (int i=r;i>mid;i--)
{
while (j>=l&&q[j].x>q[i].x) { add(q[j].y,q[j].val); j--; }
ans[q[i].id]+=q[i].val*sum(q[i].y);
}
for (int i=mid;i>j;i--) add(q[i].y,-q[i].val);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++) { cin>>a[i]; pos[a[i]]=i; q[++tme]={i,a[i],1,1}; }
for (int i=1,ea;i<=m;i++) { cin>>ea; q[++tme]={pos[ea],ea,i+1,-1}; }
cdq(1,tme);
for (int i=1;i<=m;i++) { ans[i]+=ans[i-1]; cout<<ans[i]<<"\n"; }
return 0;
}
例题1.3
看似只是HH的项链再加一维限制,但在维护
咕了咕了
CDQ可以用来解决三维数点,也可以优化DP
例题2.1
这里就是要求最长不上升子序列。设状态
这是一个三维偏序,所以可以用 CDQ 优化
可以用树状数组维护一个区间最大值,然后直接转移。最后第一个答案就是
第二个答案是啥呢。就是包含
要注意这里的
然后这里和 CDQ 解决三维数点的分治顺序有所不同。众所周不知,若
大概就是细节很多的样子
码
#include <bits/stdc++.h>
#define int long long
#define pid pair<int,double>
#define mkp make_pair
#define fst first
#define scd second
using namespace std;
const int N=5e4+5;
int n,cnt,b[N];
struct node { int id,x,y; }q[N],q2[N];
pid ans,f1[N],f2[N];
pid operator +(pid a,pid b)
{
if (a.fst>b.fst) return a;
else if (a.fst<b.fst) return b;
else return mkp(a.fst,a.scd+b.scd);
}
struct BIT
{
pid tr[N];
int lowbit(int x) { return x&-x; }
void add(int x,pid y) { while (x) { tr[x]=tr[x]+y; x-=lowbit(x); } }
void clear(int x) { while (x) { tr[x]=mkp(0,0); x-=lowbit(x); } }
pid query(int x)
{
pid res=mkp(0,0);
while (x<=cnt) { res=res+tr[x]; x+=lowbit(x); }
return res;
}
}Tr;
void init()
{
sort(b+1,b+n+1);
cnt=unique(b+1,b+1+n)-b-1;
for (int i=1;i<=n;i++) q[i].y=lower_bound(b+1,b+1+cnt,q[i].y)-b;
for (int i=1;i<=n;i++) f1[i]=f2[i]=mkp(1,1);
}
bool cmp1(node x,node y) { return x.x>y.x; }
bool cmp2(node x,node y) { return x.id<y.id; }
bool cmp3(node x,node y) { return x.id>y.id; }
void cdq1(int l,int r)
{
if (l==r) return ;
int mid=(l+r)>>1;
cdq1(l,mid);
sort(q+l,q+mid+1,cmp1),sort(q+mid+1,q+r+1,cmp1);
int j=l;
for (int i=mid+1;i<=r;i++)
{
while (j<=mid&&q[j].x>=q[i].x) { Tr.add(q[j].y,f1[q[j].id]); j++; }
pid ea=Tr.query(q[i].y);
f1[q[i].id]=f1[q[i].id]+mkp(ea.fst+1,ea.scd);
}
for (int i=l;i<j;i++) Tr.clear(q[i].y);
sort(q+l,q+r+1,cmp2),cdq1(mid+1,r);
}
void cdq2(int l,int r)
{
if (l==r) return ;
int mid=(l+r)>>1;
cdq2(l,mid);
sort(q2+l,q2+mid+1,cmp1),sort(q2+mid+1,q2+r+1,cmp1);
int j=l;
for (int i=mid+1;i<=r;i++)
{
while (j<=mid&&q2[j].x>=q2[i].x) { Tr.add(q2[j].y,f2[q2[j].id]); j++; }
pid ea=Tr.query(q2[i].y);
f2[q2[i].id]=f2[q2[i].id]+mkp(ea.fst+1,ea.scd);
}
for (int i=l;i<j;i++) Tr.clear(q2[i].y);
sort(q2+l,q2+r+1,cmp3),cdq2(mid+1,r);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1;i<=n;i++) { q[i].id=i; cin>>q[i].x>>q[i].y; b[i]=q[i].y; }
init(),cdq1(1,n);
sort(q+1,q+1+n,cmp2);
for (int i=1;i<=n;i++) q2[n-i+1]={i,cnt-q[i].x+1,cnt-q[i].y+1};
sort(q2+1,q2+1+n,cmp3),cdq2(1,n);
for (int i=1;i<=n;i++) ans=ans+f1[i];
cout<<ans.fst<<"\n";
for (int i=1;i<=n;i++)
{
if (f1[i].fst+f2[i].fst-1==ans.fst) cout<<fixed<<setprecision(5)<<(1.0*f1[i].scd*f2[i].scd)/ans.scd<<" ";
else cout<<"0.00000 ";
}
return 0;
}
2024.12.22-图匹配
霍尔定理:
设二分图
必要性证明:感性理解
充分性证明:数学归纳法证明
然后就有
好渴……我要喝水……我要喝水啊啊啊啊吸吸吸吸吸
例1【exhausted 精疲力尽的】
转化题意,就是求对于每个子集(人),求子集大小减去陪集大小的最大值,
人集的陪集就是所有
然后待会儿再说
匈牙利算法:
匈牙利算法利用增广路求最大匹配
增广路 O(nm):
-
对于一个匹配
,若存在起始于非匹配点,结束于除起点外的非匹配点,且路径上由匹配边(在 中)和非匹配边 (不在 中)交错形成的简单路径 ,则称 是相对于 的一条增广路 -
显然,在增广路上匹配边比非匹配边少1,所以对一个增广路进行取反操作可以得到一个更大的匹配
-
所以,
是该图的最大匹配当且仅当不存在相对于 的增广路
匈牙利算法就是一直找增广路,每找到一条新的增广路就翻转变新的
搜索增广路形象过程:男女配对,边是好感关系,只能有好感的男女配对,一男只能对一女。每次从男向女配,若该男的一个好感对象已有搭子,就让搭子换,让该女与该男配对;若其搭子换不了,那就该男换
1.1 板子 二分图最大匹配
板子,直接做做完了
板
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=510;
int n,m,num,ans;
vector <int> e[N];
int mch[N],vis[N];
bool dfs(int x)
{
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (vis[v]) continue;
vis[v]=1;
if (!mch[v]||dfs(mch[v])) { mch[v]=x; return true; }
}
return false;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>num;
for (int i=1,u,v;i<=num;i++)
{
cin>>u>>v;
e[u].push_back(v);
}
for (int i=1;i<=n;i++)
{
memset(vis,0,sizeof vis);
if (dfs(i)) ans++;
}
cout<<ans;
return 0;
}
1.2【连续攻击游戏】
增广路上匹配点非匹配点交错出现,所以可以用个虚点连接一个装备的两种属性,这些虚点属于一个点集,属性属于另一个点集。然后从
码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5;
int n,mx,idx[N],mch[N];
vector <int> e[N];
bool dfs(int x,int tme)
{
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (idx[v]==tme) continue;
idx[v]=tme;
if (!mch[v]||dfs(mch[v],tme)) { mch[v]=x; return true; }
}
return false;
}
int calc()
{
int res=0;
for (int i=1;i<=n;i++)
{
if (dfs(i,i)) res++;
else break;
}
return res;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1,u,v;i<=n;i++)
{
cin>>u>>v;
e[u].push_back(i+10000);
e[v].push_back(i+10000);
e[i+10000].push_back(u),e[i+10000].push_back(v);
mx=max({mx,u,v});
}
cout<<calc();
return 0;
}
最小点覆盖&最大独立集
当且仅当图上的每一条边至少有一个端点在点集中,这个点集被称为该图的点覆盖
在二分图中,最小点覆盖等于最大匹配
证明
不会,见wiki
当且仅当该点集中任意两点不被边直接连接时,这个点集被称为该图的独立集;在任意图上,点覆盖的补集都是独立集,所以它俩的并集就是所有点,它俩大小和就是点数
DAG最小路径覆盖&DAG最小链覆盖
-
最小路径覆盖要求找到若干条路径,使得每个点都被恰好一条路径包含,并使得路径数量最小
-
做法:把一个点掰成两半:出和入,对于一条边
,让 出向 入连边,让所有出点和入点分别形成二分图的点集。
要使得路径数量最小,就要使得路径上边数最大,最后答案就是 n - 二分图最大匹配(首先显然这最大匹配的非完全增广路是一个路径,剩下的不在这个非完全增广路上的边是一段有标记,另一端没标记的边,只能自己成一个路径不然就不合法)
最小链覆盖和最小路径覆盖很像,但允许路径间有交,此时最小链覆盖相当于在原 DAG 上做完传递闭包后的最小路径覆盖,感性理解叭
Dilworth定理
一个偏序集上最小链覆盖大小等于最长反链长度,DAG 上最小链覆盖大小等于最长反链长度,都是 n - 传递闭包二分图最大匹配啦……
反链:DAG 传递闭包后的独立集?
2.1【组合】
最小链覆盖等于最长反链……在这道题中还等于最大独立集
有几个宝藏就给它掰成几个元素,这样相当于是求最小链覆盖的个数了
然后这题"没边",可以直接 dp ……直接设状态
好短的代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1005;
int T,n,m;
int a[N][N],f[N][N];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>T;
while (T--)
{
memset(f,0,sizeof f);
cin>>n>>m;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++) cin>>a[i][j];
for (int i=1;i<=n;i++)
for (int j=m;j>0;j--) f[i][j]=max({f[i-1][j+1]+a[i][j],f[i][j+1],f[i-1][j]});
cout<<f[n][1]<<"\n";
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效