• 一些Trick


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

    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;
    }
    
  • 相关阅读:
    vue+element ui 表格自定义样式溢出隐藏
    vue自定义指令directives使用及生命周期
    前端如何下载文件
    js实现活动倒计时
    echarts自定义提示框数据
    vue项目如何刷新当前页面
    数据库——关于索引
    Javascript节点选择
    asp.net 身份验证(Update)
    ASP.config配置
  • 原文地址:https://www.cnblogs.com/Troverld/p/14605754.html
Copyright © 2020-2023  润新知