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 只需要分裂和合并操作就可以完成各种查询。其中,分裂有按排名分裂与按值分裂,按值分裂是将原本的树分裂成两棵根分别为 l,r 的树,l 树所有权值都小于等于 xr 树所有权值都大于 x 。按排名分裂就是前 x 个数在 l 树中,后面的数都在 r 树里。合并一般是将两棵权值/排名有序的平衡树按照 rand 值合并。

按排名分裂的平衡树中序遍历是原数组,按值分裂的平衡树中序遍历是有序的元素。所以一般来说,无法同时按排名分裂、按值分裂?

嘻嘻

#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 文艺平衡树(区间翻转)

有个结论:对于一棵平衡树,交换所有节点的左右子树后的中序遍历相当于是翻转区间后的序列。手模易得,不会证

所以,给每个节点打上一个 tag ,代表该节点是否要翻转,然后就没然后了

板*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 还是区间翻转

你说得对,但是这题第一眼看过去感觉就是要同时权值分裂、排名分裂

然后题解区出来了一个无比聪明的想法:按排名建树,将普通平衡树的 rand 值赋为高度,那么每次平衡树的根就是最小值所在的位置,翻转左子树再删去这个根就 ending 了

当然了,这是肯定能被 hack 掉的(原本序列就具有单调性的时候平衡树就会退化成链),然后就蹦出来了笛卡尔树单调栈优化建树……这题不用也能过,不多说了

警示自己:先下传标记再返回!!!

时刻牢记 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学长来的一天。

上次讲了平衡树,这次讲了二维数点。

二维数点

二维数点,就是在二维平面上数点的个数

一般是把一维在线操作转化为二维离线的操作,查询的时间就是新的一维(也有其他的转化)


呃啊


比如,给出一个序列a,求对于i[l,r],满足aix的个数。

1.1 板子

这里就是将下标i作为一个维度,将值ai作为另一个维度,每次相当于查询满足x[l,r],y[,x](x,y)的个数。

联想到前缀和,要求这个矩阵的点数,相当于是四个四分之一平面的答案经过一番加减之后的结果。

(这题中更简单,直接用两个查询相减即可,也就是将一个询问拆成俩query(l,r)变成query(r)query(l1),眼熟眼熟)

道理我都懂,但我还是不知道怎么实现啊

引入一个我不会的扫描线

之前一直听扫描线啥的,这啊那啊的,我现在终于知道是咋扫的了www

扫描线:每次根据一个维度进行有序扫描,扫描的过程中进行操作。

在这道题中就是扫描下标,每次有两种操作:加点,和查询。加点就是把ai加进去,查询就是查询当前满足aix的个数,这用树状数组是好维护的。但有些查询对答案是负贡献,所以离线处理的时候也需要存储贡献正负。

然后就有了

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记录dfn序、子树大小、深度等。

对于m次操作,注意到对于每个操作2,只有上一个操作1会对它产生影响,所以将会对该操作产生影响的操作1与操作2捆绑在一起。

每次按照dep降序扫描,先将该层的点都加进去,再计算储存的操作的贡献。储存操作是按照操作1中的深度储存的而不是操作2的

基本上一遍过的代码
#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的项链

每个区间相同的元素贡献一次就可以。发现第一次在区间出现的元素 ai 的上一个相同元素一定在区间之前,即在 [1,l1] ,之后的元素的上一个相同元素在区间 [l,r] 内。所以可以记上一个相同元素所在的位置 prei ,该区间 [l,r] 内的贡献就是 prei[1,l1] 的数的个数。然后直接做做完了

#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 扫描线:矩形面积并

居然不是二维数点,我还想半天……

首先 显然 ,根据出入边把混合图形掰成若干个矩形然后计算。用线段树维护当前横截面的长度,最后的答案就是每个横截面长度乘横坐标差值的和。

因为每个横截面只需要计算一次,即多加了也计算一次,所以线段树中记一个 tag 表示当前区间是否有数。如果有,那么直接赋值区间长度(离散化前),否则就左右子树 push_up。最后查询的事整个区间的,和子区间没啥关系,所以可以免去 push_down 下传懒惰标记

#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个(八分之一空间),常数较大

去看了会儿并查集,结果感觉漏掉了挺重要的事情……?优化常数的?

对于(ai,bi,ci)(Ai,Bi,Ci)的立方体查询,搞成八分之一空间答案就是

(Ai,Bi,Ci)

(ai1,Bi,Ci)(Ai,bi1,Ci)(Ai,Bi,ci1)

+(Ai,bi1,ci1)+(ai1,Bi,ci1)+(ai1,bi1,Ci)

(ai1,bi1,ci1)

超好理解的


做法有CDQ分治,树套树,KDT

k维数点可以KDT或CDQ套CDQ套CDQ……

CDQ分治

解决多为限制问题 O(log n)解决一维

在值域上不断 分治。对于一维,先噶出一个mid,每次只处理xi<mid的修改操作对于ximid的查询的贡献

小剪枝:对于一个区间,没有任何操作就return掉

然后就固定了一维,就变成了二维数点

三维数点模板

好困好困,难以思考。

用 CDQ 求出 f(i) 、再桶一下 f(i) 就做完了

先把所有输入排个序,然后直接 CDQ 。对于每个操作区间 l,r ,将 [l,mid] 归为插入, [mid+1,r] 归为查询。因为已经按第一关键字排序了,所以已经卸去了一重限制。

剩下的“二维数点”其实不用完全按扫描线那样做。可以再分别给两个操作区间按第二关键字排序,两个区间有序后双指针统计。

因为会对每个操作区间再排个序,排序后两个区间的第一关键字相对有序,但内部顺序被打乱,所以要先处理小区间再处理该区间

另外,注意回溯,以及答案要存结构体里!

#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

很简,按照时间分治就行了。

但是在“前缀和”时, x11 可能是 0 ,用过树状数组的人都知道传进去个 0 会发生什么……所以输入的所有坐标都要 +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 逆序对喵

你说得对,但不是所有删点都需要逆向变加点的

你说得对,但这种形似“每次插入都需要查询所有”可以只计算这次插入对答案产生的新的贡献,最后前缀和一下就好了,不用一次插入后跟着 n 个查询……

你说得对,但插入有时候确实能把查询的事儿也干了,模板不也是这样做的吗……

我好菜
#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的项链再加一维限制,但在维护 pre 时有一堆东西要处理

咕了咕了


CDQ可以用来解决三维数点,也可以优化DP

例题2.1

这里就是要求最长不上升子序列。设状态 fi 表示以 i 结尾的最长不上升子序列的长度,那么显然有状态转移方程

fi=maxj=1i1fj+1(hjhi,vjvi)

这是一个三维偏序,所以可以用 CDQ 优化

可以用树状数组维护一个区间最大值,然后直接转移。最后第一个答案就是 maxi=1nfi

第二个答案是啥呢。就是包含 i 的方案数除以总方案数。跑两遍 DP ,分别记录以 i 结尾的最大长度和以 i 开头的最大长度及其方案数 g1i,g2i ,那么概率就是 g1i×g2itol ,其中 tol 是总方案数。

要注意这里的 tol 可能很大,会爆 long long ……因为取概率的时候不咋要求精度,所以可以用 double 来存

然后这里和 CDQ 解决三维数点的分治顺序有所不同。众所周不知,若 fifj 转移,那么 fj 应是已经确定了的。在三维偏序问题中,一般是先计算 [l,mid],[mid+1,r] ,然后再算 [l,r] ;但在此处要是也用这样的分治顺序的话,在处理 [mid+1,r] 时因为左半拉没有转移完(未被 [l,mid] 更新过),所以它的右半拉的 DP 值就会出问题……也就是说,应该先治 [l,mid] ,然后是 [l,r] ,最后是 [mid+1,r] 。但这样的话在排序时可能会破坏偏序关系问题……所以要记得还原

大概就是细节很多的样子

#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-图匹配

作业链接

霍尔定理:

设二分图 G={V1,V2,E},|V1|<|V2|,二分图存在完美匹配,当且仅当对于 VV1V 的陪集 S 满足 |S||V|

必要性证明:感性理解
充分性证明:数学归纳法证明

超绝霍尔定理直观证明

然后就有

好渴……我要喝水……我要喝水啊啊啊啊吸吸吸吸吸

例1【exhausted 精疲力尽的】

转化题意,就是求对于每个子集(人),求子集大小减去陪集大小的最大值,n 减去人集的最大值就是所求答案

人集的陪集就是所有 [1,li][ri,m] 的并集。不好考虑,于是考虑取所有不可取集合 (li,ri) 的交集,最后取个补集。

然后待会儿再说

匈牙利算法:

匈牙利算法利用增广路求最大匹配

增广路 O(nm):

  • 对于一个匹配 M ,若存在起始于非匹配点,结束于除起点外的非匹配点,且路径上由匹配边(在 M 中)和非匹配边 (不在 M 中)交错形成的简单路径 P ,则称 P 是相对于 M 的一条增广路

  • 显然,在增广路上匹配边比非匹配边少1,所以对一个增广路进行取反操作可以得到一个更大的匹配 M

  • 所以,M 是该图的最大匹配当且仅当不存在相对于 M 的增广路

匈牙利算法就是一直找增广路,每找到一条新的增广路就翻转变新的 M ,直到找不出为止

搜索增广路形象过程:男女配对,边是好感关系,只能有好感的男女配对,一男只能对一女。每次从男向女配,若该男的一个好感对象已有搭子,就让搭子换,让该女与该男配对;若其搭子换不了,那就该男换

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【连续攻击游戏】

增广路上匹配点非匹配点交错出现,所以可以用个虚点连接一个装备的两种属性,这些虚点属于一个点集,属性属于另一个点集。然后从 1 开始跑增广路就行,因为要求是连续的,要是跑不出来就已经不合法了,不要再跑增广路

#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最小链覆盖

  • 最小路径覆盖要求找到若干条路径,使得每个点都被恰好一条路径包含,并使得路径数量最小

  • 做法:把一个点掰成两半:出和入,对于一条边 (u,v) ,让 u 出向 v 入连边,让所有出点和入点分别形成二分图的点集。

    要使得路径数量最小,就要使得路径上边数最大,最后答案就是 n - 二分图最大匹配(首先显然这最大匹配的非完全增广路是一个路径,剩下的不在这个非完全增广路上的边是一段有标记,另一端没标记的边,只能自己成一个路径不然就不合法)

最小链覆盖和最小路径覆盖很像,但允许路径间有交,此时最小链覆盖相当于在原 DAG 上做完传递闭包后的最小路径覆盖,感性理解叭

Dilworth定理

一个偏序集上最小链覆盖大小等于最长反链长度,DAG 上最小链覆盖大小等于最长反链长度,都是 n - 传递闭包二分图最大匹配啦……

反链:DAG 传递闭包后的独立集?

2.1【组合】

最小链覆盖等于最长反链……在这道题中还等于最大独立集

有几个宝藏就给它掰成几个元素,这样相当于是求最小链覆盖的个数了

然后这题"没边",可以直接 dp ……直接设状态 fi,j 表示从右上角到 (i,j) 的最小链覆盖(也就是最大独立集);在这道题中,最大独立集就是不满足左上右下关系的点的最大权值和……也就是左下右上关系的点的权值和。所以(?)就有方程式

fi,j=max{fi,j+1,fi1,j,fi1,j+1+ai,j}

好短的代码
#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;
}

2025.3.4-Z函数

posted @   还是沄沄沄  阅读(22)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示