P7218 [JOISC2020] 伝説の団子職人

SDNetFriend

2021-11-26 15:00:48

Solution

## P7218 [JOISC2020] 伝説の団子職人 ~~模拟退火真的是太好玩了!~~ upd:发现原程序实际上是一份爬山,现添加说明并补充模拟退火代码。 upd:取消了 NP-Hard 的 LaTeX。 ### 题意这里不再赘述 [P7218 [JOISC2020] 伝説の団子職人](https://www.luogu.com.cn/problem/P7218) ### 做法分析 我们可以看出这个问题可以转化成一般图的最大独立集。 即我们把几串共用了相同位置的丸子连边,表示它们不能同时存在,于是这个问题就转化完成了。 我们知道这是个 NP-Hard 问题,那就需要考虑模拟退火。 先按一串一串地把图建好。然后退火过程中,每次选一个没选的点,选中它并退掉与它相连的已选中的点,**并更新与退掉的点相连的那些点**(这一步很重要不然答案越跑越劣)。 然后前面几篇题解是用数据结构维护没选的点每次随机排名,实际上直接纯随就可以,一直随直到随到一个没选的点,实测命中率还是相当高的。 ### 建图 这里是考虑每个中间的 `W`,分别讨论四个方向。只要保证两边都不是 `W` 并且不同即可。 对于连边这里在每个点上开了个 `vector` 存包含该点串的编号。 (似乎是现有题解区里最短的代码了) ### 调参 这个玩意调了我一上午,主要是模拟退火要保证接受劣解的概率不要太高,实在不行给 $d$ 乘上一个系数 $k$ 来放大代价,不然就会越跑越劣甚至干不过爬山。~~模拟退化算法~~ 因为后来发现通过的实际上已经退化成了爬山,所以这里不再给出所有的参数,仅放出第五个点相对较优的参数: `#5:T=6,dta=0.999995,k=16` 不知道为什么爬山能过,所以这里放出两份代码。 ### 贴代码 #### 爬山: ```cpp #include <bits/stdc++.h> #define lint long long using namespace std; inline int read(){ char c;int f=1,res=0; while(c=getchar(),!isdigit(c))if(c=='-')f*=-1; while(isdigit(c))res=res*10+c-'0',c=getchar(); return res*f; } const int N=505,P=N*N*8; int hed[P],nxt[P],ver[P],cnt=1; inline void ae(int u,int v){ ver[++cnt]=v; nxt[cnt]=hed[u]; hed[u]=cnt; } inline void lnk(int u,int v) {ae(u,v);ae(v,u);} vector<int> ex[N][N]; int d[4][2]={{0,1},{1,0},{1,1},{1,-1}}; char c[4]={'-','|','\\','/'}; struct pnt{int x,y,d;}p[P]; int n,m,tot=0;char s[N][N]; inline void inst(int x,int y,int dir){//加点 int x0=x+d[dir][0],y0=y+d[dir][1]; int x1=x-d[dir][0],y1=y-d[dir][1]; if(x0<1||y0<1||x1<1||y1<1)return; if(x0>n||x1>n||y0>m||y1>m)return; if(s[x0][y0]=='W'||s[x1][y1]=='W') return; if(s[x][y]!='W'||s[x0][y0]==s[x1][y1]) return; int u=++tot;p[u]=pnt{x,y,dir}; for(int v:ex[x][y])lnk(u,v);ex[x][y].push_back(u); for(int v:ex[x0][y0])lnk(u,v);ex[x0][y0].push_back(u); for(int v:ex[x1][y1])lnk(u,v);ex[x1][y1].push_back(u); } inline void build(){//建图 for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) for(int k=0;k<4;++k) inst(i,j,k); } int ans; bitset<P> vis,_vis,avis; inline bool ck(int u){//判断当前点能否选 if(_vis[u])return false; for(int e=hed[u];e;e=nxt[e]) if(_vis[ver[e]])return false; return true; } inline int calc(){//产生初始解 int res=0; for(int u=1;u<=tot;++u) if(ck(u))_vis[u]=1,++res; return res; } inline int rnd(int md) {return 1ll*rand()*rand()%md+1;} inline int del(int u){ _vis[u]=0;int res=0; for(int e=hed[u];e;e=nxt[e]) if(ck(ver[e]))_vis[ver[e]]=1,++res; return res; } inline int upd(){//产生新解 _vis=vis; int res=0,t=0; while(++t<=10){//没什么用的防死循环 int u=rnd(tot); if(_vis[u])continue; _vis[u]=1;++res; for(int e=hed[u];e;e=nxt[e]){ int v=ver[e]; if(_vis[v])res+=del(v)-1; //退掉与u相连的v并更新与v相连的点 }break; }return res; } inline void SA(){ ans=calc();vis=_vis; int res=ans,_res; double T=0.01,dta=0.999999,k=16; while(ans<48620){ _res=res+upd(); if(_res>ans)//也许这里不是比较全局答案,但好像这样跑得快些 avis=vis=_vis,ans=res=_res,cout<<T<<" "<<ans<<endl; else if(exp(k*(_res-ans)/T)*RAND_MAX>rand()) vis=_vis,res=_res; T*=dta; } } int main(){ freopen("05.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;++i) scanf("%s",s[i]+1); build();SA(); for(int i=1;i<=tot;++i) if(avis[i]) s[p[i].x][p[i].y]=c[p[i].d]; freopen("05.ans","w",stdout); for(int i=1;i<=n;++i) printf("%s\n",s[i]+1); return 0; } ``` #### 模拟退火: ```cpp #include <bits/stdc++.h> #define lint long long using namespace std; inline int read(){ char c;int f=1,res=0; while(c=getchar(),!isdigit(c))if(c=='-')f*=-1; while(isdigit(c))res=res*10+c-'0',c=getchar(); return res*f; } const int N=505,P=N*N*8; int hed[P],nxt[P],ver[P],cnt=1; inline void ae(int u,int v){ ver[++cnt]=v; nxt[cnt]=hed[u]; hed[u]=cnt; } inline void lnk(int u,int v) {ae(u,v);ae(v,u);} vector<int> ex[N][N]; int d[4][2]={{0,1},{1,0},{1,1},{1,-1}}; char c[4]={'-','|','\\','/'}; struct pnt{int x,y,d;}p[P]; int n,m,tot=0;char s[N][N]; inline void inst(int x,int y,int dir){//加点 int x0=x+d[dir][0],y0=y+d[dir][1]; int x1=x-d[dir][0],y1=y-d[dir][1]; if(x0<1||y0<1||x1<1||y1<1)return; if(x0>n||x1>n||y0>m||y1>m)return; if(s[x0][y0]=='W'||s[x1][y1]=='W') return; if(s[x][y]!='W'||s[x0][y0]==s[x1][y1]) return; int u=++tot;p[u]=pnt{x,y,dir}; for(int v:ex[x][y])lnk(u,v);ex[x][y].push_back(u); for(int v:ex[x0][y0])lnk(u,v);ex[x0][y0].push_back(u); for(int v:ex[x1][y1])lnk(u,v);ex[x1][y1].push_back(u); } inline void build(){//建图 for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) for(int k=0;k<4;++k) inst(i,j,k); } int ans; bitset<P> vis,_vis,avis; inline bool ck(int u){//判断当前点能否选 if(_vis[u])return false; for(int e=hed[u];e;e=nxt[e]) if(_vis[ver[e]])return false; return true; } inline int calc(){//产生初始解 int res=0; for(int u=1;u<=tot;++u) if(ck(u))_vis[u]=1,++res; return res; } inline int rnd(int md) {return 1ll*rand()*rand()%md+1;} inline int del(int u){ _vis[u]=0;int res=0; for(int e=hed[u];e;e=nxt[e]) if(ck(ver[e]))_vis[ver[e]]=1,++res; return res; } inline int upd(){//产生新解 _vis=vis; int res=0,t=0; while(++t<=10){//没什么用的防死循环 int u=rnd(tot); if(_vis[u])continue; _vis[u]=1;++res; for(int e=hed[u];e;e=nxt[e]){ int v=ver[e]; if(_vis[v])res+=del(v)-1; //退掉与u相连的v并更新与v相连的点 }break; }return res; } inline void SA(){ ans=calc();vis=_vis; int res=ans,_res; double T=6,dta=0.999995,k=16; while(ans<48620){ _res=res+upd(); if(_res>=res){ if(_res>ans) ans=_res,avis=_vis,cout<<T<<" "<<_res<<endl; vis=_vis;res=_res; }else if(exp(k*(_res-res)/T)*2e9>rnd(2e9)) vis=_vis,res=_res; T*=dta; } } int main(){ ios::sync_with_stdio(false); freopen("05.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;++i) scanf("%s",s[i]+1); build();SA(); for(int i=1;i<=tot;++i) if(avis[i]) s[p[i].x][p[i].y]=c[p[i].d]; freopen("05.ans","w",stdout); for(int i=1;i<=n;++i) printf("%s\n",s[i]+1); return 0; } ```