本文包含一些常见算法的使用技巧。

I.树上最小拓扑序(瞎起名字*1)

本方法适用于一类问题,它要求对一棵树求出它的某种拓扑序\(\{p\}\),使得对于排列定义的函数\(w\Big(\{p\}\Big)\)有\(\min/\max\)。

具体来说,我们会发现这个拓扑序中有一些点,是会在父亲节点被选入拓扑序后立即被选上的。这种时候,我们就可以将该节点与父亲合并(使用并查集)。我们每次挑选最优的一对父子进行合并,便能保证总结果最优。关于这个“最优”的定义同\(w\)函数有关。

下面我们来看几道例题:

I.I.[POJ2054]Color a Tree

 

\[w\Big(\{p\}\Big)=\sum\limits_{i=1}^nic_{p_i} \]

 

我们考虑一对父子,它们合并后会增加什么:设一个合并后的节点的\(c\)的总和为\(sum\),它是\(num\)个节点的合并;则有当一对父子\((x,y)\)合并的时候,有答案增加\(num_x\times sum_y\)。(关于这个增加方式,它利用了差分——每一次合并都将\(y\)集合内部的所有点的位置往后顺延了\(num_x\)位)

当我们考虑两个儿子\(u\)和\(v\)谁该先被合并的时候,我们考虑一下:

当\(u\)排在\(v\)前面的时候,最终答案会增加\(num_u\times sum_v\);

反之,会增加\(num_v\times sum_u\)。

当两个交换位置时,其它位置的贡献并不会有改变;故我们只需要挑出两个式子中较小的那个让它排在前面即可。

我们发现,这实际上就是找出\(\dfrac{sum_x}{num_x}\)最大的那一个合并;故可以直接用一个std::set维护即可。

时间复杂度\(O(n\log n)\)。

代码:

#include<stdio.h>
#include<set>
#include<vector>
using namespace std;
int n,m,res,fa[1010],sum[1010],num[1010],dsu[1010];
vector<int>v[1010];
void dfs(int x){for(int i=0;i<v[x].size();i++)if(v[x][i]!=fa[x])fa[v[x][i]]=x,dfs(v[x][i]);}
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
struct node{
	int x;
	node(int X){x=X;}
	friend bool operator <(const node &x,const node &y){return sum[x.x]*num[y.x]!=sum[y.x]*num[x.x]?sum[x.x]*num[y.x]>sum[y.x]*num[x.x]:x.x>y.x;}
};
set<node>s;
void merge(int x,int y){
	x=find(x),y=find(y);
	if(x!=m)s.erase(node(x));
	res+=sum[y]*num[x],dsu[y]=x,sum[x]+=sum[y],num[x]+=num[y];
	if(x!=m)s.insert(node(x));
}
int main(){
	while(scanf("%d%d",&n,&m)){
		if(!n&&!m)break;
		res=0;
		for(int i=1;i<=n;i++)scanf("%d",&sum[i]),dsu[i]=i,num[i]=1,fa[i]=0,v[i].clear();
		for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
		dfs(m);
		for(int i=1;i<=n;i++)if(i!=m)s.insert(node(i));
		while(!s.empty()){
			int x=s.begin()->x;s.erase(s.begin());
			merge(fa[x],x);
		}
		printf("%d\n",res+sum[m]);		
	}
	return 0;
} 

I.II.[AGC023F] 01 on Tree

 

\[w\Big(\{p\}\Big)=\{p\}\text{中逆序对数} \]

 

我们考虑每个节点维护它所代表的集合中\(0\)的个数和\(1\)的个数,设为\(zero_i\)与\(one_i\);则当一对父子\((x,y)\)合并时,答案就增加\(one_x\times zero_y\)。

然后就和上一题一样了。

代码:

#include<stdio.h>
#include<set>
#include<vector>
using namespace std;
typedef long long ll;
int n,m,fa[200100],one[200100],zero[200100],dsu[200100];
ll res;
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
struct node{
	int x;
	node(int X){x=X;}
	friend bool operator <(const node &x,const node &y){
		ll X=1ll*one[x.x]*zero[y.x],Y=1ll*one[y.x]*zero[x.x];
		return X!=Y?X<=Y:x.x<y.x;
	}
};
set<node>s;
void merge(int x,int y){
	x=find(x),y=find(y);
	if(x!=m)s.erase(node(x));
	res+=1ll*zero[y]*one[x],dsu[y]=x,zero[x]+=zero[y],one[x]+=one[y];
	if(x!=m)s.insert(node(x));
}
int main(){
	scanf("%d",&n);
	for(int i=2;i<=n;i++)scanf("%d",&fa[i]);
	for(int i=1,x;i<=n;i++)scanf("%d",&x),dsu[i]=i,(x?one[i]:zero[i])++;
	for(int i=2;i<=n;i++)s.insert(i);
	while(!s.empty()){
		int x=s.begin()->x;s.erase(s.begin());
		merge(fa[x],x);
	}
	printf("%lld\n",res);
	return 0;
} 
II.正难则反(并非瞎起的名字)

正难则反在很多场景下都有应用,下面的分项会介绍具体的用法。

II.I.期望题中的正难则反

期望题是其重要的应用场景之一。这部分题的题解可以见概率期望学习笔记

II.I.I.[SDOI2012]走迷宫

将从起点出发转成从终点出发。

II.I.II.[HNOI2011]XOR和路径

类似。

III.“最小字典序”在DP时的体现
  1. 考虑其转移过程中能否体现出最小字典序的思想(即能否建图后直接bfs/Dijkstra转移)
  2. 考虑记录路径,并从终点状态出发倒着bfs亦可
  3. 考虑压缩路径(典型例子:如果路径是一个排列,考虑康托展开;如果路径是一个数组,考虑一些trick例如压成\(k\)进制的数)。此种trick特别适用于状压DP的情形,因为路径长度很短
  4. 永远,永远不要想着把整条路径全部记录下来并\(O(n)\)比较!!!(除非你确定这样比较复杂度正确)
IV.二维数据之积的小trick(瞎起名字*2)

我们有时候会碰到这样的一类题目:给定一些物品,物品有两个属性\(a,b\),并且只有符合某些条件的物品集合是合法的。要求最小化\(\Big(\sum a\Big)\times\Big(\sum b\Big)\)。

如果最优的物品集合在只有一维时很易求出,则此种trick有效:

我们考虑对于每个集合,将其映射为平面直角坐标系中一个点,其中\(x\)坐标为\(\sum a\),\(y\)坐标为\(\sum b\)。则我们如果将所有可能集合全部映射到坐标系中,只有其下凸包上的点可能成为最优答案。凭直觉我们会发现凸包上的点不会很多,具体有多少我们接下来会分析。

我们考虑求出所有集合中,\(x\)坐标最小的一个(此时其\(y\)坐标显然会最大)以及\(y\)坐标最小的一个(此时其\(x\)坐标显然会最大)。则显然此两点肯定在下凸包上。

我们现在考虑求出这个凸包上其它点。考虑设当前凸包两端的点分别为\(A\)和\(B\)(默认\(A\)的\(x\)坐标更小)。则我们希望找到的点\(C\)应该满足:

  1. 在直线\(AB\)下方

  2. 在所有的同类节点中,距\(AB\)最远

明显只有这样的点\(C\)可以确保一定在下凸包上。

因为\(|AB|\)固定,所以我们实际上要最大化\(S_{\triangle ABC}\)。考虑将其转成叉积形式——

\((B-C)\times(A-C)\)

暴力拆开之后:

\((x_B-x_A)y_C+(y_A-y_B)x_C-(x_B-x_A)y_A-(y_A-y_B)x_A\)

明显后两项与\(C\)无关。则我们只需最小化前两项即可。明显前两项可以拆开计算,即设物品的新权值为\(b(x_B-x_A)+a(y_A-y_B)\),然后按照一维算法即可求出。

在求出\(C\)后,我们就可以继续分治处理区间\((A,C)\)和\((C,B)\),直到新的\(C\)已经不在\(AB\)下方。

考虑其复杂度。明显,即为\(O(\text{凸包上点数}\times\text{一维算法的复杂度})\)。那么,凸包上究竟会有多少点呢?

据说是\(O(\text{值域}^{2/3})\)的。咱也不知道详细证明,反正只需要会用就行了

下面我们看几道例题:

IV.I.[BalkanOI2011] timeismoney | 最小乘积生成树

一维算法是最小生成树时的情形。代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Vector{
	int x,y;
	Vector(int X=0,int Y=0){x=X,y=Y;}
	friend bool operator<(const Vector &u,const Vector &v){return 1ll*u.x*u.y==1ll*v.x*v.y?u.x<v.x:1ll*u.x*u.y<1ll*v.x*v.y;}
	friend Vector operator +(const Vector &u,const Vector &v){return Vector(u.x+v.x,u.y+v.y);}
	friend Vector operator -(const Vector &u,const Vector &v){return Vector(u.x-v.x,u.y-v.y);}
	void operator +=(const Vector &v){x+=v.x,y+=v.y;}
	void operator -=(const Vector &v){x-=v.x,y-=v.y;}
	friend ll operator *(const Vector &u,const Vector &v){return 1ll*u.x*v.y-1ll*u.y*v.x;}
}res=Vector(0x3f3f3f3f,0x3f3f3f3f);
int n,m,u[10100],v[10100],a[10100],b[10100],ord[10100],dsu[210];
ll c[10100];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
bool merge(int x,int y){
	x=find(x),y=find(y);
	if(x==y)return false;
	dsu[y]=x;
	return true;
}
Vector calc(){
	sort(ord+1,ord+m+1,[](int x,int y){return c[x]<c[y];});
	for(int i=1;i<=n;i++)dsu[i]=i;
	Vector ret;
//	for(int i=1;i<=m;i++)printf("%d ",mst[i]);puts("");
	for(int i=1;i<=m;i++)if(merge(u[ord[i]],v[ord[i]]))ret+=Vector(a[ord[i]],b[ord[i]]);
	res=min(res,ret);
	return ret;
}
void solve(Vector x,Vector y){
	for(int i=1;i<=m;i++)c[i]=1ll*(y.x-x.x)*b[i]+1ll*(x.y-y.y)*a[i];
	Vector z=calc();
	if((x-z)*(y-z)>=0)return;
	solve(x,z),solve(z,y);
} 
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)scanf("%d%d%d%d",&u[i],&v[i],&a[i],&b[i]),ord[i]=i,u[i]++,v[i]++;
	for(int i=1;i<=m;i++)c[i]=a[i];
	Vector x=calc();
	for(int i=1;i<=m;i++)c[i]=b[i];
	Vector y=calc();
	solve(x,y);
	printf("%d %d\n",res.x,res.y);
	return 0;
}

IV.II.[HNOI2014]画框

一维算法是二分图最小权完美匹配的情形。网络流无法通过,不得不现学KM算法

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Vector{
	int x,y;
	Vector(int X=0,int Y=0){x=X,y=Y;}
	friend Vector operator +(const Vector &u,const Vector &v){return Vector(u.x+v.x,u.y+v.y);}
	friend Vector operator -(const Vector &u,const Vector &v){return Vector(u.x-v.x,u.y-v.y);}
	void operator +=(const Vector &v){x+=v.x,y+=v.y;}
	void operator -=(const Vector &v){x-=v.x,y-=v.y;}
	friend ll operator *(const Vector &u,const Vector &v){return 1ll*u.x*v.y-1ll*u.y*v.x;}
};
ll res;
int T_T,n,m,a[200][200],b[200][200],c[200][200],lw[200],d[200],pre[200],mat[200];
bool vis[200];
queue<int>q;
bool bfs(int S){
	while(!q.empty())q.pop();
	q.push(S);
	while(!q.empty()){
		int x=q.front();q.pop(),vis[x]=true;
//		printf("%dPRPR\n",x);
		for(int y=n+1;y<=2*n;y++){
			if(c[x][y]!=lw[x]+lw[y]||vis[y])continue;
			vis[y]=true;
			if(mat[y])pre[mat[y]]=x,q.push(mat[y]);
			else{
				int u=x,v=y;
				while(u){
//					printf("%d %d\n",u,v);
					int tmp=mat[u];
					mat[u]=v,mat[v]=u;
					u=pre[u],v=tmp;
				}
				return true;
			}
		}
	}
	return false;
}
void KM(){
	memset(lw,0,sizeof(lw)),memset(mat,0,sizeof(mat)),memset(pre,0,sizeof(pre));
	for(int i=1;i<=n;i++)for(int j=n+1;j<=2*n;j++)lw[i]=max(lw[i],c[i][j]);
	for(int i=1;i<=n;i++){
//		printf("%d\n",i);
		memset(vis,false,sizeof(vis)),memset(d,0x3f,sizeof(d));
		if(bfs(i))continue;
		for(int j=1;j<=n;j++)if(vis[j])for(int k=n+1;k<=2*n;k++)if(!vis[k])d[k]=min(d[k],lw[j]+lw[k]-c[j][k]);
		while(true){
			int now=0x3f3f3f3f,nex;
			for(int j=n+1;j<=2*n;j++)if(!vis[j])now=min(now,d[j]);
			for(int j=1;j<=n;j++)if(vis[j])lw[j]-=now;
			for(int j=n+1;j<=2*n;j++)if(vis[j])lw[j]+=now;else d[j]-=now,nex=d[j]?nex:j;
//			puts("NI");
			if(!mat[nex])break;
			vis[nex]=vis[mat[nex]]=true;
			nex=mat[nex];
			for(int j=n+1;j<=2*n;j++)if(!vis[j])d[j]=min(d[j],lw[nex]+lw[j]-c[nex][j]);
		}
//		puts("FIN");
		memset(vis,false,sizeof(vis));
		bfs(i);
	}
}
Vector calc(Vector ip){
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)c[i][j+n]=-ip.x*b[i][j]-ip.y*a[i][j];
	KM();
	Vector ret;
	for(int i=1;i<=n;i++)ret+=Vector(a[i][mat[i]-n],b[i][mat[i]-n]);
	res=min(res,1ll*ret.x*ret.y);
	return ret;
}
void solve(Vector x,Vector y){
	Vector z=calc(Vector(y.x-x.x,x.y-y.y));
	if((x-z)*(y-z)>=0)return;
	solve(x,z),solve(z,y);
} 
void read(int &x){
	x=0;
	char c=getchar();
	while(c>'9'||c<'0')c=getchar();
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
int main(){
	read(T_T);
	while(T_T--){
		read(n),res=0x3f3f3f3f3f3f3f3f;
		for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)read(a[i][j]);
		for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)read(b[i][j]);
		Vector x=calc(Vector(0,1)),y=calc(Vector(1,0));
		solve(x,y);
		printf("%lld\n",res);
	}
	return 0;
}