[学习笔记] 邓老师调整法
本篇博客和邓老师论文的区别就是不严谨有代码。
简介
组合优化问题有如下形式:一个问题有一些合法解和不合法解,每个合法解有一个对应的权值,你需要在所有合法解中找出权值最大的一个。
一种显然的做法是:先任取一个合法解,然后对合法解进行微调使得权值变大,一直操作直到无法进行。这一算法看似简单,但在许多问题中有出色的表现。
值得注意的是,这种调整方法得到的答案是不降的,它并不像模拟退火一样有一定概率接受劣解,所以调整方式不能让当前解被局限(能通过答案不降的路径调整到最优解\(/\)较优解)
下面将结合不同的题目类型讲解邓老师调整法的应用,着重体会其思想。
一般图最大匹配
我们考虑一个未匹配的点集 \(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]);
}