天天看點

20210914下午

20210914下午

搜尋?狗都不用。

20210914下午
T1 T2 T3 T4 T5
預測 100 100 100 60
一測 100 100 100

T1:

簡單搜尋。。。按順序搜就行

T2:

提示把剪枝都講了,實在是太良心了。

T3:

不是,憑什麼 n = 8000 n=8000 n=8000能搜出來啊。。。我本來想寫 10 10 10個 h a s h hash hash暴力了。。。

20210914下午

爆搜能搜出來。。。而且題面是 N o    s o l u t i o n ! No\ \ solution! No  solution!,答案是 N o    S o l u t i o n ! No\ \ Solution! No  Solution!。

T4:

經典搜尋加模拟,其實蠻簡單的,隻有消去和墜落兩個操作要注意。

一些剪枝:

1.如果目前同一顔色隻有 1 1 1塊或 2 2 2塊就結束遊戲,無解。

2.隻有右邊為空時才往左邊移,否則一定往右移更優。

3.如果是相同色塊就無需交換。

貼個代碼吧。

struct game{ //遊戲狀态
	int s[10][10]; //塊色
	int col[15];  //每種顔色個數,用于剪枝1
	bool cl[10][10];  //清除标記
	void Read()  //讀入
	{
		int tmp;
		for(int i=1;i<=5;i++) for(int j=1;scanf("%d",&tmp)&&tmp;j++) s[i][j]=tmp;
		return;
	}
	bool empty() //是否消完
	{
		for(int i=1;i<=5;i++) for(int j=1;j<=7;j++) if(s[i][j]) return false;
		return true;
	}
	bool clear() //消塊
	{
		bool res=false;
		for(int i=1;i<=5;i++)
		{
			for(int j=1,k,c;j<=7;j++)
			{
				c=s[i][j];if(!c) continue;
				for(k=i;k<=5&&s[k+1][j]==c;k++); //向右拓展
				if(k-i+1>=3) {res=true;for(int l=i;l<=k;l++) cl[l][j]=true;}
				for(k=j;k<=7&&s[i][k+1]==c;k++); //向上拓展
				if(k-j+1>=3) {res=true;for(int l=j;l<=k;l++) cl[i][l]=true;}
			}
		}
		for(int i=1;i<=5;i++) for(int j=1;j<=7;j++) if(cl[i][j]) s[i][j]=0,cl[i][j]=false; //消去
		return res;
	}
	void fall()  //墜落
	{
		for(int j=2;j<=7;j++) for(int i=1;i<=5;i++) for(int k=j;k>1&&s[i][k]&&!s[i][k-1];k--) swap(s[i][k],s[i][k-1]);  //下面為空就一直掉
		return;
	}
	bool over() //剪枝1
	{
		memset(col,0,sizeof(col));
		for(int i=1;i<=5;i++) for(int j=1;j<=7;j++) col[s[i][j]]++;
		for(int i=1;i<=10;i++) if(col[i]>=1&&col[i]<=2) return true;
		return false;
	}
}st;


void Dfs(int x,game now) //搜尋
{
	if(flag) return;
	now.fall(); //可能上次移向空處 
	while(now.clear()) now.fall(); //能消就一直消
	if(now.over()) return; 
	if(x==n+1)
	{
		if(now.empty()) print(),flag=true;
		return;
	}
	game nxt;nxt=now;
	for(int i=1;i<=5;i++)
	{
		for(int j=1;j<=7;j++)
		{
			if(!now.s[i][j]) continue;
			if(in(i+1,j)&&now.s[i+1][j]!=now.s[i][j]) //剪枝3
			{
				swap(nxt.s[i+1][j],nxt.s[i][j]);
				ans[x].x=i;ans[x].y=j;ans[x].f=1;
				Dfs(x+1,nxt);
				if(flag) return;
				swap(nxt.s[i+1][j],nxt.s[i][j]);
			}
			if(!now.s[i-1][j]) //剪枝2
			{
				if(!in(i-1,j)||now.s[i-1][j]==now.s[i][j]) continue; //剪枝3
				swap(nxt.s[i-1][j],nxt.s[i][j]);
				ans[x].x=i;ans[x].y=j;ans[x].f=-1;
				Dfs(x+1,nxt);
				if(flag) return;
				swap(nxt.s[i-1][j],nxt.s[i][j]);
			}
		}
	}
	return;
}
           

思路很清晰的模拟。

T5:

《關于我用搜尋怎麼剪都剪不過就去學 D a n c i n g    L i n k s    X Dancing\ \ Links\ \ X Dancing  Links  X然後 40 m s 40ms 40msA了數獨這件事》

20210914下午

搜尋我就不多說了,就算我加了按最小選擇排序也過不了,可能是我的問題。

下面我來隆重介紹 D a n c i n g    L i n k s    X Dancing\ \ Links\ \ X Dancing  Links  X算法。

D L X DLX DLX是由 X X X算法通過十字連結清單優化而來,用于解決精準覆寫問題。

什麼是精準覆寫問題?

可以了解為給出 n n n個集合和一個全集,要求選出一些集合使他們不重複地構成全集,也就是 S 1 ∪ S 2 ∪ S 3 ⋯ = S S_1\cup S_2 \cup S_3 \dots =S S1​∪S2​∪S3​⋯=S且 S 1 ∩ S 2 ∩ S 3 ⋯ = ∅ S_1 \cap S_2 \cap S_3 \dots=\varnothing S1​∩S2​∩S3​⋯=∅。

舉個例子,現在有集合

S = { 114 , 233 , 514 , 7 , 1926 , 817 , 666 } S=\{114,233,514,7,1926,817,666\} S={114,233,514,7,1926,817,666}

S 1 = { 114 , 514 , 7 } S_1=\{114,514,7\} S1​={114,514,7}

S 2 = { 114 , 1926 , 817 , 666 } S_2=\{114,1926,817,666\} S2​={114,1926,817,666}

S 3 = { 514 , 817 , 666 } S_3=\{514,817,666\} S3​={514,817,666}

S 4 = { 114 } S_4=\{114\} S4​={114}

S 5 = { 114 , 233 , 7 , 1926 } S_5=\{114,233,7,1926\} S5​={114,233,7,1926}

S 6 = { 514 , 7 , 1926 , 66 } S_6=\{514,7,1926,66\} S6​={514,7,1926,66}

S 7 = { 233 , 514 , 7 } S_7=\{233,514,7\} S7​={233,514,7}

S 8 = { 233 , 817 } S_8=\{233,817\} S8​={233,817}

可以一眼看出這個問題的精準覆寫為 S 2 S_2 S2​和 S 7 S_7 S7​或 S 3 S_3 S3​和 S 5 S_5 S5​或 S 4 S_4 S4​, S 6 S_6 S6​和 S 8 S_8 S8​。

那麼 X X X算法是如何解決這個問題的呢?

首先,我們把這些關系轉化為矩陣

S 114 233 514 7 1926 817 666 S 1 1 0 1 1 0 0 0 S 2 1 0 0 0 1 1 1 S 3 0 0 1 0 0 1 1 S 4 1 0 0 0 0 0 0 S 5 1 1 0 1 1 0 0 S 6 0 0 1 1 1 0 1 S 7 0 1 1 1 0 0 0 S 8 0 1 0 0 0 1 0 \begin{matrix} S&114&233&514&7&1926&817&666 \\ S_1&1&0&1&1&0&0&0 \\ S_2&1&0&0&0&1&1&1 \\ S_3&0&0&1&0&0&1&1 \\ S_4&1&0&0&0&0&0&0 \\ S_5&1&1&0&1&1&0&0 \\ S_6&0&0&1&1&1&0&1 \\ S_7&0&1&1&1&0&0&0 \\ S_8&0&1&0&0&0&1&0 \end{matrix} SS1​S2​S3​S4​S5​S6​S7​S8​​11411011000​23300001011​51410100110​710001110​192601001100​81701100001​66601100100​

然後,我們先假設選擇了 S 1 S_1 S1​,顯然我們就不能選那些在 S 1 S_1 S1​上有數的集合,那麼我們就去删除這些行和列

20210914下午

得到

S 233 1926 817 66 S 8 1 0 1 0 \begin{matrix} S&233&1926&817&66 \\ S_8 &1&0&1&0 \end{matrix} SS8​​2331​19260​8171​660​

接着我們選擇 S 8 S_8 S8​并進行同樣的操作。

S 1926 66 \begin{matrix} S&1926&66 \end{matrix} S​1926​66​

發現集合被删完了, S S S卻還有剩,說明選 S 1 S_1 S1​無法得到答案,于是回溯恢複被删除的行和列并不再考慮 S 1 S_1 S1​

之後選擇 S 2 S_2 S2​

20210914下午

得到

S 233 514 7 S 7 1 1 1 \begin{matrix} S&233&514&7 \\ S_7&1&1&1 \end{matrix} SS7​​2331​5141​71​

選擇 S 7 S_7 S7​, S S S被删完,說明找到一組答案,若不是多組答案可以直接退出程式了。

若要找多組答案,則繼續回溯,選擇 S 3 S_3 S3​

20210914下午

得到

S 114 233 7 1926 S 4 1 0 0 0 S 5 1 1 1 1 \begin{matrix} S&114&233&7&1926 \\ S_4&1&0&0&0 \\ S_5&1&1&1&1 \end{matrix} SS4​S5​​11411​23301​701​192601​

若選擇 S 4 S_4 S4​,那麼有

S 233 7 1926 \begin{matrix} S&233&7&1926 \end{matrix} S​233​7​1926​

無解,回溯選擇 S 5 S_5 S5​,發現消完,找到另一組解。

回溯選擇 S 4 S_4 S4​

20210914下午

得到

S 233 514 7 1926 817 666 S 6 0 1 1 1 0 1 S 7 1 1 1 0 0 0 S 8 1 0 0 0 1 0 \begin{matrix} S&233&514&7&1926&817&666 \\ S_6&0&1&1&1&0&1 \\ S_7&1&1&1&0&0&0 \\ S_8&1&0&0&0&1&0 \end{matrix} SS6​S7​S8​​233011​514110​7110​1926100​817001​666100​

選擇 S 6 S_6 S6​,得到

S 233 817 S 8 1 1 \begin{matrix} S&233&817 \\ S_8&1&1 \end{matrix} SS8​​2331​8171​

删去後再次得到一組解。

後面就不模拟了(做圖做麻了,不過大概意思應該都知道了,這就是 X X X算法的大緻流程。

可以發現此算法需要大量的删除行列和恢複的操作,是以如果樸素實作,其指數級複雜度取決于矩陣中 1 1 1的個數,大概隻能接受 n , m ≤ 100 n,m\le 100 n,m≤100的矩陣。

是以我們的十字連結清單優化就來啦, D L X DLX DLX橫空出世。(其實并沒有,它也不是啥省選内容。

下面偷一下DLX 詳細講解的圖來簡單介紹一下十字連結清單(我真的做不來啊淦。

20210914下午

十字連結清單擁有四個指針左 l l l右 r r r上 u u u下 d d d,以這些指針來存儲一個矩陣。

完整版大概是張這樣的。

20210914下午

當然,除了上面的幾個數組,還有 f i r s t i first_i firsti​表示每一行的行首,每一列的列首則是由 0 0 0節點連接配接出的虛拟節點。

另外需要一個 s i z i siz_i sizi​記錄每列的節點個數。

是以大概需要這幾個數組

int n,m,tot,first[N],siz[N]; //tot表示節點編号
int l[N],r[N],u[N],d[N];
int row[N],col[N]; //表示該節點的行列
           

接下來是幾個操作

1.建表

建構一個 R × C R\times C R×C的空矩陣,隻需将每一列的虛節點建出來就行。

void build(int R,int C)
{
	n=R;m=C;
	for(int i=0;i<=C;i++) l[i]=i-1,r[i]=i+1,u[i]=d[i]=i;
	l[0]=C;r[C]=0;tot=C; //每一行列都是環狀連結清單,新節點從C+1開始
	memset(first,0,sizeof(first));
	memset(siz,0,sizeof(siz));
	return;
}
           

另外建議使用以下形式通路環狀連結清單

2.插入

在 ( R , C ) (R,C) (R,C)處插入一個點,相當于我們在矩陣内插入一個 1 1 1。

定義該節點為 t o t tot tot,在列上,直接進行操作。

将 t o t tot tot上方改為 C C C, t o t tot tot下方改為原來 C C C下方的節點,原來 C C C下方節點的上方改為 t o t tot tot, C C C下方節點改為 t o t tot tot。

對行,進行分類。

若該行還沒有 f i r s t first first節點,将 f i r s t first first直接改為 t o t tot tot。

若該行有 f i r s t first first節點了,将 t o t tot tot左邊改為 f i r s t R first_R firstR​,右邊改為原來 f i r s t R first_R firstR​的右邊, f i r s t R first_R firstR​右邊節點的左邊改為 t o t tot tot, f i r s t R first_R firstR​的右邊改為 t o t tot tot

if(!first[R]) first[R]=l[tot]=r[tot]=tot;
else l[tot]=first[R],r[tot]=r[first[R]],l[r[first[R]]]=tot,r[first[R]]=tot;
           

都是很好了解的連結清單操作,完整如下

void insert(int R,int C)
{
	row[++tot]=R;col[tot]=C;siz[C]++;
	u[tot]=C,d[tot]=d[C],u[d[C]]=tot,d[C]=tot;
	if(!first[R]) first[R]=l[tot]=r[tot]=tot;
	else l[tot]=first[R],r[tot]=r[first[R]],l[r[first[R]]]=tot,r[first[R]]=tot;
	return;
}
           

3.移除

對于移除列 C C C的操作,我們需要移除這一列與向下通路到的每一行。

移除這一列很簡單,将 C C C左右節點的左右指針修改即可。

接着向下通路每個節點,再橫向通路哪一行,将節點上下節點修改以删去那一行。

完成修改操作。

void remove(int C)
{
	l[r[C]]=l[C];r[l[C]]=r[C];
	IT(i,d,C) IT(j,r,i) u[d[j]]=u[j],d[u[j]]=d[j],siz[col[j]]--;
	return;
}
           

4.恢複

其實就是把移除操作反過來弄一遍。

void recover(int C)
{
	IT(i,d,C) IT(j,r,i) u[d[j]]=d[u[j]]=j,siz[col[j]]++;
	l[r[C]]=r[l[C]]=C;
	return;
}
           

以上就是我們需要的所有操作,然後與 X X X算法結合就是 D L X DLX DLX算法了。

bool dance(int dep,int *s)
{
	int C=r[0]; //若0無右節點,說明矩陣為空,算法結束
	if(!C) {···;return true;} //處理答案
	IT(i,r,0) if(siz[i]<siz[C]) C=i; //找一個結點最少的列以減少搜尋樹
	remove(C);
	IT(i,d,C)
	{
		s[dep]=row[i];
		IT(j,r,i) remove(col[j]);
		if(dance(dep+1,s)) return true; //鏡像操作,若多組資料就不用return
		IT(j,r,i) recover(col[j]);
		s[dep]=0;
	}
	recover(C);	
	return false;
}
           

(叫 D a n c i n g    L i n k s    X Dancing\ \ Links\ \ X Dancing  Links  X是因為搜尋過程在各行中跳)

可以通過【模闆】舞蹈鍊(DLX)了。

D L X DLX DLX的複雜度為 O ( c n ) O(c^n) O(cn),其中 n n n為矩陣中 1 1 1的個數,而 c c c是一個十分接近于 1 1 1的常數,是以一般可以通過大部分題目。

不過該算法還有一個難點,将題目要求抽象成集合與矩陣。

我們回到數獨這道題本身上來。

在 i i i行 j j j列 w w w宮 ( i , j , w ) (i,j,w) (i,j,w)填一個數 k k k,因為 w w w可以由 i , j i,j i,j确定,是以我們隻需要 i × j × k i\times j\times k i×j×k即 9 × 9 × 9 = 729 9\times 9\times 9=729 9×9×9=729行(個集合)。

而這個操作又會引起哪些擾動呢。

會使 ( i , j ) (i,j) (i,j)位置被占

會使 i i i行 k k k這個數無法使用。

會使 j j j列 k k k這個數無法使用。

會使 w w w宮 k k k這個數無法使用。

每種都需要 81 81 81列來存儲,是以共 81 × 4 = 324 81\times 4=324 81×4=324列(集合大小)。

是以,我們成功将數獨問題轉化為一個 729 × 324 729\times 324 729×324的精确覆寫問題。

代碼和模闆的差別在于插入結點。

#include<bits/stdc++.h>
#define IT(i,A,x) for(int i=A[x];i!=x;i=A[i]) //好用
using namespace std;
const int N=1e5+5; //數組要開矩陣大小
int a,s[N],ans[105][105];
class Orthogonal_List{ //十字連結清單
	public:
		int n,m,tot,first[N],siz[N];
		int l[N],r[N],u[N],d[N];
		int col[N],row[N];
		void build(int R,int C)
		{
			n=R;m=C;
			for(int i=0;i<=C;i++) l[i]=i-1,r[i]=i+1,u[i]=d[i]=i;
			l[0]=C;r[C]=0;tot=C;
			memset(first,0,sizeof(first));
			memset(siz,0,sizeof(siz));
			return;
		}
		void insert(int R,int C)
		{
			row[++tot]=R;col[tot]=C;siz[C]++;
			u[tot]=C,d[tot]=d[C],u[d[C]]=tot,d[C]=tot;
			if(!first[R]) first[R]=l[tot]=r[tot]=tot;
			else l[tot]=first[R],r[tot]=r[first[R]],l[r[first[R]]]=tot,r[first[R]]=tot;
			return;
		}
		void remove(int C)
		{
			l[r[C]]=l[C];r[l[C]]=r[C];
			IT(i,d,C) IT(j,r,i) u[d[j]]=u[j],d[u[j]]=d[j],siz[col[j]]--;
			return;
		}
		void recover(int C)
		{
			IT(i,d,C) IT(j,r,i) u[d[j]]=d[u[j]]=j,siz[col[j]]++;
			l[r[C]]=r[l[C]]=C;
			return;
		}
		bool dance(int dep)
		{
			int C=r[0];
			if(!C) {for(int i=1,x,y,z;i<dep;i++) {x=((s[i]-1)/9)/9+1;y=((s[i]-1)/9)%9+1;z=(s[i]-1)%9+1;ans[x][y]=z;};return true;}
			IT(i,r,0) if(siz[i]<siz[C]) C=i;
			remove(C);
			IT(i,d,C)
			{
				s[dep]=row[i];
				IT(j,l,i) remove(col[j]); //這裡用兩個IT(j,r,i)就會錯,其他都行,玄學。。。
				if(dance(dep+1)) return true;
				IT(j,r,i) recover(col[j]);
				s[dep]=0;
			}
			recover(C);
			return false;
		}
}lis;
char ch[100];
int main()
{
	while(scanf("%s",ch+1)&&ch[1]!='e')
	{
		lis.build(729,324); //建圖
		for(int i=1;i<=9;i++)
		{
			for(int j=1;j<=9;j++)
			{
				a=ch[(i-1)*9+j]=='.'?0:ch[(i-1)*9+j]-'0';
				for(int k=1,l;k<=9;k++)
				{
					if(a-k&&a) continue; //其實就是有值就隻填a,為空就每個都填
					l=((i-1)*9+(j-1))*9+k; //行
					lis.insert(l,(i-1)*9+j); //i行j列
					lis.insert(l,81+(i-1)*9+k); //i行k數
					lis.insert(l,162+(j-1)*9+k); //j列k數
					lis.insert(l,243+(((i-1)/3)*3+(j-1)/3)*9+k); //w宮k數
				}
			}
		}
		lis.dance(1); //你你你你要跳舞嗎
		for(int i=1;i<=9;i++) for(int j=1;j<=9;j++) putchar(ans[i][j]+'0');putchar('\n');
	}
	return 0;
}
           

完結撒花。

總結:搜尋是有極限的,是以我直接擺爛啦。

20210914下午
上一篇: 4.20下午
下一篇: 3.17下午

繼續閱讀