[学习笔记] 邓老师调整法

本篇博客和邓老师论文的区别就是不严谨有代码。

简介

组合优化问题有如下形式:一个问题有一些合法解和不合法解,每个合法解有一个对应的权值,你需要在所有合法解中找出权值最大的一个。

一种显然的做法是:先任取一个合法解,然后对合法解进行微调使得权值变大,一直操作直到无法进行。这一算法看似简单,但在许多问题中有出色的表现。

值得注意的是,这种调整方法得到的答案是不降的,它并不像模拟退火一样有一定概率接受劣解,所以调整方式不能让当前解被局限(能通过答案不降的路径调整到最优解\(/\)较优解)

下面将结合不同的题目类型讲解邓老师调整法的应用,着重体会其思想。

一般图最大匹配

点此看题

我们考虑一个未匹配的点集 \(V\),每次我们从中随机出一个点 \(u\),然后考虑 \(u\) 的邻接点中是否存在未配对的。如果存在那么随机一个直接匹配,答案\(+1\);否则我们随机一个已匹配的点 \(v\),断开 \(v\) 和其匹配点的边,然后把 \((u,v)\) 作为新的匹配加入。

一直重复上述步骤到超时为止,很可惜的是无法通过 \(\tt extra\ test\)

#include <cstdio>
#include <vector>
#include <cstdlib>
#include <iostream> 
#include <ctime>
using namespace std;
const int M = 505; 
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,ans,mat[M],s[M];vector<int> g[M];
int random(int x)
{
	return (1ll*rand()*rand()+rand())%x;
}
void zxy()
{
	k=0;
	for(int i=1;i<=n;i++)
		if(!mat[i] && !g[i].empty()) s[++k]=i;
	if(!k) return ;//perfect!
	int u=s[random(k)+1];k=0;
	for(int v:g[u]) if(!mat[v])
		s[++k]=v;
	if(k)//random a match node
	{
		int v=s[random(k)+1];
		mat[u]=v;mat[v]=u;return ;
	}
	//adjust it
	for(int v:g[u]) s[++k]=v;
	int v=s[random(k)+1];
	mat[mat[v]]=0;
	mat[u]=v;mat[v]=u;
}
signed main()
{
	n=read();m=read();srand(time(0));
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	while(1.0*clock()/CLOCKS_PER_SEC<=0.95) zxy();
	for(int i=1;i<=n;i++)
		if(mat[i]) ans++;
	printf("%d\n",ans/2);
	for(int i=1;i<=n;i++)
		printf("%d ",mat[i]);
}

隐式匹配问题

在信息学竞赛中,另有一大类问题与匹配相关,但常常无法转化成常规的一般图最大匹配,我们称其为隐式匹配问题。对于这类问题,前文提及的调整算法常常奏效。尽管复杂度并没有严格的证明,但往往有出色的实际表现。

CF1168E Xor Permutations

点此看题

首先有一个简单的问题转化,我们原来是把 \(p,q\) 匹配到 \(a\),根据异或的特性我们把 \(p\)\(a\) 匹配到 \(q\),那么要求是找出一个排列,使得其和 \(x\) 的异或两两不同

我们随机一个 \(p\) 中还未匹配的正整数,然后看它和 \(a\) 中哪个位置匹配后的值还没有出现过。设这个位置为 \(y\)(如果有多个随机一个),那么我们把 \((x,y)\) 加入匹配;如果不存在这样的 \(y\) 那么我们随机一个未匹配的位置 \(z\),然后把对应值原来的匹配断开,把 \((x,z)\) 加入匹配中。

最后说一句,本题的正解当然是构造,有解的充要条件是:\(a\) 的异或和为 \(0\)

#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <iostream>
#include <ctime>
using namespace std;
const int M = (1<<12)+5;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,sum,ans,a[M],mp[M],mx[M],x[M],vis[M];
int random(int x)
{
	return (rand()*rand()+rand())%x;
}
void zxy()
{
	m=0;
	for(int i=0;i<n;i++)
		if(mp[i]==-1) a[++m]=i;
	if(!m) return ;
	int u=a[random(m)+1];m=0;
	for(int i=0;i<n;i++)
		if(mx[i]==-1 && !vis[u^x[i]])
			a[++m]=i;
	if(m)//random match
	{
		int v=a[random(m)+1];
		mp[u]=v;mx[v]=u;
		vis[u^x[v]]=1;ans++;
		return ;
	}
	for(int i=0;i<n;i++)
		if(mx[i]==-1) a[++m]=i;
	int v=a[random(m)+1];
	//rip the former match
	for(int i=0;i<n;i++)
		if(~mp[i] && (i^x[mp[i]])==(u^x[v]))
		{
			mx[mp[i]]=-1;mp[i]=-1;
			mp[u]=v;mx[v]=u;return ;
		}
}
signed main()
{
	n=1<<read();srand(time(0));
	for(int i=0;i<n;i++) mx[i]=mp[i]=-1;
	for(int i=0;i<n;i++)
		sum^=x[i]=read();
	if(sum) {puts("Fou");return 0;}
	while(1.0*clock()/CLOCKS_PER_SEC<=0.95) zxy();
	puts("Shi");
	for(int i=0;i<n;i++) printf("%d ",mx[i]);
	puts("");
	for(int i=0;i<n;i++) printf("%d ",mx[i]^x[i]);
}

制作团子

点此看题

每次我们随机一个未匹配的 \(\tt W\) 色点,并考虑以其为中心的竹签。如果存在一个竹签上不包含匹配点,我们在这样的竹签中随机选择一个添加。否则枚举以它为中心、且只与一根已有竹签冲突的竹签,我们有一半概率用新竹签替换原来的。

下面给出我的实现,如果每个测试点跑 \(\tt 10s\) 可以在 \(\tt uoj\) 上获得 \(78\) 分,我相信跑更久能获得更多分数,所以这份实现虽然算不上好但是也大体没有问题,仅供参考。

#include <cstdio>
#include <vector>
#include <cstdlib>
#include <algorithm>
#include <ctime> 
using namespace std;
const int M = 505;
#define pb push_back
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,ans,cnt,o[4],a[M][M],t[M][M];
int x[M*M],y[M*M];char s[M][M];
struct node{int x,y,id;};vector<node> v;
int random(int x)
{
	return (rand()*rand()+rand())%x;
}
int get(int &x,int &y,int c,int f)//f\in {-1,1}
{
	if(c==0) x+=f;
	if(c==1) y+=f;
	if(c==2) x+=f,y+=f;
	if(c==3) x+=f,y-=f;
	return x>=1 && y>=1 && x<=n && y<=m;
}
void clear(int i)
{
	int &c=t[x[i]][y[i]];
	int x1=x[i],y1=y[i],x2=x1,y2=y1;
	get(x1,y1,c,-1);get(x2,y2,c,1);
	t[x1][y1]=t[x2][y2]=c=-1;
	v.pb(node{x[i],y[i],i}); 
}
void zxy()
{
	if(v.empty()) return ;
	int len=v.size(),p=random(len);
	swap(v[p],v[len-1]);
	int x=v.back().x,y=v.back().y,id=v.back().id;
	for(int i=0;i<4;i++) o[i]=i;
	random_shuffle(o,o+4);
	for(int c=0;c<4;c++)
	{
		int i=o[c],x1=x,y1=y,x2=x,y2=y;
		if(!get(x1,y1,i,-1) || !get(x2,y2,i,1))
			continue;
		if((a[x1][y1]^a[x2][y2])==3 &&
			t[x1][y1]==-1 && t[x2][y2]==-1)
		//successfully matched
		{
			ans++;
			v.pop_back();
			t[x1][y1]=t[x2][y2]=id;
			t[x][y]=i;return ;
		}
	}
	for(int c=0;c<4;c++)
	{
		int i=o[c],x1=x,y1=y,x2=x,y2=y;
		if(!get(x1,y1,i,-1) || !get(x2,y2,i,1))
			continue;
		int p=1;
		if((a[x1][y1]^a[x2][y2])==3
		&& t[x1][y1]==-1 && p)
		{
			v.pop_back();
			clear(t[x2][y2]);
			t[x1][y1]=t[x2][y2]=id;
			t[x][y]=i;return ;
		}
		if((a[x1][y1]^a[x2][y2])==3
		&& t[x2][y2]==-1 && p)
		{
			v.pop_back();
			clear(t[x1][y1]);
			t[x1][y1]=t[x2][y2]=id;
			t[x][y]=i;return ;
		}
	}
}
signed main()
{
	freopen("input_01.txt","r",stdin);
	freopen("output_01.txt","w",stdout);
	n=read();m=read();srand(time(0));
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s[i]+1);
		for(int j=1;j<=m;j++)
		{
			t[i][j]=-1;
			if(s[i][j]=='P') a[i][j]=1;
			if(s[i][j]=='G') a[i][j]=2;
		}
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) if(!a[i][j])
		{
			int s=a[i][j]|a[i-1][j]
			|a[i+1][j]|a[i][j-1]|a[i][j+1]
			|a[i-1][j-1]|a[i-1][j+1]
			|a[i+1][j-1]|a[i+1][j+1];
			if(s<3) continue;
			v.pb(node{i,j,++k});
			x[k]=i;y[k]=j;
		}
	while(1.0*clock()/CLOCKS_PER_SEC<=100) zxy();
	for(int i=1;i<=n;i++,puts(""))
		for(int j=1;j<=m;j++)
		{
			if(a[i][j]!=0)
			{
				putchar(s[i][j]);
				continue;
			}
			if(t[i][j]==-1) putchar('W');
			if(t[i][j]==0) putchar('|');
			if(t[i][j]==1) putchar('-');
			if(t[i][j]==2) putchar('\\');
			if(t[i][j]==3) putchar('/');
		}
}

图染色

我们需要最小化两端点同色的边的数目。当这一数目被减少到 \(0\),我们便得到了合法的染色方案。

考察这一种调整算法:首先为每个点随机染一种颜色。之后每次随机一个点,考虑固定其余点的颜色不变,该点染不同颜色对同色边数量的贡献,在贡献最少的颜色中等概率选取一个作为该点的颜色。

有向图哈密顿链

点此看题

维护边的一个尽量大的子集,满足只考虑这些边时每个点出入度都不超过 \(1\),且不构成圈。如果子集大小达到 \(n-1\),则找到了一条哈密顿路。

考虑调整维护子集,按随机顺序考虑边,如果加入后不构成圈,且加入之后所有点度数均仍合法,则加入这条边。否则如果不构成圈,但有一个点度数不合法,则以一半概率加入并把该点相连的与新加入边矛盾的边断掉,使用 \(\tt LCT\) 判断是否成圈。

最后补充一些实现细节,为了保证随机数强度,建议使用 \(\tt mt19937\);此外的取边的时候建议把当前的所有边 \(\tt random\_shuffle\) 一遍,然后依次尝试能不能加入边集中,一定注意任何条件都不能作为删边的判据,要不然很可能调整不出来。

现在我的代码可以跑出前 \(9\) 组数据,最后一个点还在跑,做提答题要有一定的耐心,至少要给 \(1\) 分钟跑。

#include <cstdio>
#include <vector>
#include <random>
#include <cstdlib>
#include <cassert>
#include <cstring>
#include <iostream>
#include <ctime>
using namespace std;
const int M = 500005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,cnt,in[M],out[M],vis[M];
int zxy,zp[M],p[M],fa[M],ch[M][2],fl[M],st[M];
struct node{int x,y,id;}a[M],b[M];vector<node> g[M];
int random(int x)
{
	static mt19937 fuck(time(0));
	return fuck()%x;
}
//link-cut-tree
int chk(int x)
{
    return ch[fa[x]][1]==x;
}
int nrt(int x)
{
    return ch[fa[x]][0]==x || ch[fa[x]][1]==x;
}
void flip(int x)
{
	if(!x) return ;
	swap(ch[x][0],ch[x][1]);fl[x]^=1;
}
void down(int x)
{
    if(!x) return ;
    if(fl[x])
	{
		flip(ch[x][0]);
		flip(ch[x][1]);
		fl[x]=0;
	}
}
void rotate(int x)
{
    int y=fa[x],z=fa[y],k=chk(x),w=ch[x][k^1];
    ch[y][k]=w;fa[w]=y;
    if(nrt(y)) ch[z][chk(y)]=x;fa[x]=z;
    ch[x][k^1]=y;fa[y]=x;
}
void splay(int x)
{
    int z=x,t=0;st[++t]=z;
    while(nrt(z)) z=fa[z],st[++t]=z;
    while(t) down(st[t--]);
    while(nrt(x))
    {
    	int y=fa[x];
    	if(nrt(y))
    	{
    		if(chk(x)==chk(y)) rotate(y);
    		else rotate(x);
		}
		rotate(x);
	}
}
void access(int x)
{
	for(int y=0;x;x=fa[y=x])
		splay(x),ch[x][1]=y;
}
void makert(int x)
{
	access(x);splay(x);flip(x);
}
void link(int x,int y)
{
	makert(x);fa[x]=y;
}
void cut(int x,int y)
{
	makert(x);access(y);splay(x);
	ch[x][1]=fa[y]=0;
}
int findrt(int x)
{
	access(x);splay(x);
	while(ch[x][0]) down(x),x=ch[x][0];
	splay(x);return x;
}
void work()
{
	swap(a[random(m)+1],a[m]);
	int u=a[m].x,v=a[m].y,id=a[m].id;
	if(findrt(u)==findrt(v)) return ;
	if(!out[u] && !in[v])
	{
		out[u]=in[v]=id;link(u,v);
		ans++;m--;return ;
	}
	if(out[u] && in[v]) return ;
	int h=random(2);
	if(out[u] && h)
	{
		int p=b[out[u]].x,q=b[out[u]].y;
		a[m]=b[out[u]];
		cut(p,q);out[p]=in[q]=0;
		out[u]=in[v]=id;link(u,v);
		return ;
	}
	if(h)
	{
		int p=b[in[v]].x,q=b[in[v]].y;
		a[m]=b[in[v]];
		cut(p,q);out[p]=in[q]=0;
		out[u]=in[v]=id;link(u,v);
	}
}
void dfs(int u)
{
	p[++cnt]=u;
	for(node t:g[u]) if(!vis[t.id])
		dfs(t.x);
}
signed main()
{
	freopen("hamil10.in","r",stdin);
	freopen("hamil10.out","w",stdout);
	n=read();m=read();srand(time(0));
	for(int i=1;i<=10;i++) read();
	for(int i=1;i<=m;i++)
	{
		a[i].x=read();a[i].y=read();
		a[i].id=i;b[i]=a[i];
		g[a[i].x].push_back(node{a[i].y,0,i});
	}
	while(1.0*clock()/CLOCKS_PER_SEC<=3600) work();
	for(int i=1;i<=m;i++)
		vis[a[i].id]=1;
	for(int i=1;i<=n;i++) if(!in[i])
	{
		cnt=0;dfs(i);
		if(cnt>zxy)c++
			zxy=cnt,memcpy(zp,p,sizeof zp);
	}
	printf("%d\n",zxy);
	for(int i=1;i<=zxy;i++)
		printf("%d ",zp[i]);
}
posted @ 2022-02-24 22:34  C202044zxy  阅读(750)  评论(2编辑  收藏  举报