Java計算手機九宮格鎖屏圖案連接9個點的方案總數


(一)問題

九宮格圖案解鎖連接9個點共有多少種方案?

(二)初步思考

可以把問題抽象為求滿足一定條件的1-9的排列數(類似於“八皇后問題”),例如123456789和987654321都是合法的(按照從上到下、從左到右、從1到9編號),解決此類問題一般都用遞歸方法,因為問題規模較大,且沒有明確的計算方法

(三)深度思考

不難想出下面的簡單思路:

1.先窮舉,再排除不合法結果(過濾窮舉的結果)

大略估計一下復雜度,A99=362880(計算機應該可以接受),方案總數不超過A99,也就是說窮舉的結果是A99,再濾去不合法的結果即可,理論上此方法可行

2.按條件窮舉(在窮舉的過程中排除不合法結果)

1>正常思維

---a.選擇起點位置(i,j)

---b.在起點周圍尋找合法終點,規則如下:(共有12個方向)

------1.上(i - 1, j)--5.左上斜(i - 1, j - 1)---9.左上長斜(i - 2, j - 1)

------2.下(i + 1, j) -6.左下斜(i + 1, j - 1) -10.左下長斜(i + 2, j - 1)

------3.左(i, j - 1)--7.右上斜(i - 1, j + 1)--11.右上長斜(i - 2, j + 1)

------4.右(i, j + 1) -8.右下斜(i + 1, j + 1)-12.右下長斜(i + 2, j + 1)

---c.記錄路徑(起點 + 終點)

---d.判滿,若路徑長度小於9執行e步驟,否則執行f步驟

---e.終點變起點,返回a步驟

---f.輸出結果(路徑)

2>逆向思維

---a.排除(1.排除已選擇的點2.排除從起點不能到達的點)得到臨時剩余點集

---b.在臨時剩余點集中選擇下一個點

---c.判滿,路徑長度小於9,執行d步驟,否則執行e步驟

---d.執行a步驟

---e.輸出結果(路徑)

P.S.正常思維比較容易理解,逆向思維更容易實現

(四)編碼

[最初想用方案2的逆向思維方案來實現,結果失敗了,原因是遞歸內嵌循環,程序執行軌跡難以捉摸...頭疼良久之后放棄了,改用方案1,簡單粗暴]

[核心代碼類]

import java.util.ArrayList;  
import java.util.Arrays;  
import java.util.List;  

public class ScreenLock {
	//將NUM設置為待排列數組的長度即實現全排列 
	private int NUM = 0;
	private int count = 0;
	private String[] strSource;
	String string = null;

	private static String[] wrongPos = {//各點對應的不能到達的位置
		"379","8","179",
		"6","#","4",
		"139","2","137"};
	
	public ScreenLock(String[] strSource)
	{//strSource格式為以逗號分隔的數字串,如1,2,3,4
		//初始化
		this.strSource = strSource;
		this.NUM = strSource.length;
	}
	
	public int getCount()
	{
		sort(Arrays.asList(strSource), new ArrayList());
		
		return count;
	}

	private void sort(List datas, List target) {
		if (target.size() == NUM) {
			StringBuilder sb = new StringBuilder();
			for (Object obj : target)
				sb.append(obj);
			String ans = sb.toString();
			if(isValid(ans))
				count++;
			return;
		}
		for (int i = 0; i < datas.size(); i++) {
			List newDatas = new ArrayList(datas);
			List newTarget = new ArrayList(target);
			newTarget.add(newDatas.get(i));
			newDatas.remove(i);
			sort(newDatas, newTarget);
		}
	}

	private boolean isValid(String ans)
	{//判斷ans是否合法
		for(int i = 0;i < ans.length() - 1;i++)
		{
			//獲取當前位置的字符的值
			int currPos = Integer.parseInt(ans.charAt(i) + "");
			//獲取路徑子串
			String path = ans.substring(0, i + 1);
			//獲取錯誤值
			String wrrPos = wrongPos[currPos - 1];
			//獲取可變錯誤值
			if(currPos != 5)//5不可能變
			{
				for(int j = 0;j < wrrPos.length();j++)
				{
					//獲取中間值
					String wrong = wrrPos.charAt(j) + "";
					int mid = (currPos + Integer.parseInt(wrong)) / 2;
					//若中間值已被連接,則錯誤終點可用
					if(path.contains(mid + ""))
						wrrPos = wrrPos.replace(wrong, "#");

					//若下一位是錯誤值則ans不合法
					if(wrrPos.contains(ans.charAt(i + 1) + ""))
						return false;
				}
			}
		}

		return true;
	}
}  

特別說明:上面代碼中用到的全排列算法來自http://blog.csdn.net/sunyujia/article/details/4124011 (尊重前輩的勞動成果)

[測試類]

public class CountLockPlans {

	public static void main(String[] args) {
		//計算手機九宮格鎖屏圖案連接9個點的方案總數
		String s = "1,2,3,4,5,6,7,8,9";
		
		String[] str = s.split(",");
		ScreenLock lock = new ScreenLock(str);
		int num = lock.getCount();
		System.out.println("連接9個點共有 " + num + " 種方案");
	}

}

(五)運行結果

連接9個點共有 62632 種方案【此結果是錯的,詳情見最下方第(十)點】

(六)程序正確性檢驗

1.能否生成1-9的全排列?

注釋掉無關代碼,直接輸出所有全排列,同時計數,結果無誤(全部輸出需要17秒左右)

2.isValid()方法是否能夠正確判斷方案的合法性?

單獨調用isValid()傳入各種參數測試,結果無誤

3.算法邏輯是否無誤?

求排列的同時過濾不合法解,邏輯無誤

[綜上,理論上輸出的結果是正確的]

(七)答案正確性確認

上網找找有沒有人算出結果,濾去所有看起來不靠譜的答案,選定果殼網答案(據說用了Mathematica,乍看高上大

文章中的解決思路也是:合法方案數 = 全排列總數 - 不合法方案數

原文鏈接:http://www.guokr.com/article/49408/

仔細看過文章后發現原文的結論可能是錯的(雖然不知道其具體算法)

1.從原文的貼圖可以看到先求出了方案總數985 824(這個我們不必關心,只需要關注連接9個點的計算結果就好)

2.原文貼圖記下不能直接連的點對(和我們的wrongPos數組作用類似,用來排除)

3.接着往下看是:根據上一步的點對生成所有不合法方案(仍然不知道具體是怎么算的,但是這里肯定存在漏洞)

原作者的思路應該是按照相鄰點來判斷生成不合法方案(例如:假設第一位是1那么如果第二位選擇3,則以13開頭的所有方案全部PASS掉)

不難發現這樣一個BUG:第一位選擇2,第二位選擇1,那么第三位能不能選擇3呢?

實際情況是Yes,但如果按照上面假設的判斷方法則是No,因為(1,3)屬於不合法點對!

這就又出現新問題了,因為我們發現所謂的不合法點對好像是一個動態的集合,如果中間點被選了,那么非法點對就變成合法的了(例如2被選了之后1可以和3連接,3和1也可以連接)

我們的算法會不會存在這個問題呢?

private boolean isValid(String ans)
	{//判斷ans是否合法
		for(int i = 0;i < ans.length() - 1;i++)
		{
			//獲取當前位置的字符的值
			int currPos = Integer.parseInt(ans.charAt(i) + "");
			//獲取路徑子串
			String path = ans.substring(0, i + 1);
			//獲取錯誤值
			String wrrPos = wrongPos[currPos - 1];
			//獲取可變錯誤值
			if(currPos != 5)//5不可能變
			{
				for(int j = 0;j < wrrPos.length();j++)
				{
					//獲取中間值
					String wrong = wrrPos.charAt(j) + "";
					int mid = (currPos + Integer.parseInt(wrong)) / 2;
					//若中間值已被連接,則錯誤終點可用
					if(path.contains(mid + ""))
						wrrPos = wrrPos.replace(wrong, "#");

					//若下一位是錯誤值則ans不合法
					if(wrrPos.contains(ans.charAt(i + 1) + ""))
						return false;
				}
			}
		}

		return true;
	}

從上面的代碼不難看出我們的算法已經考慮到了這樣的情況(動態修正wrrPos)

(八)思考延伸

按照這樣的方法,我們不難算出一共有多少種方案(從四個點到九個點),在此作簡單分析:

1>9個點和8個點的數目應該相等(前8位數都定了,最后一位也就不能變了)

2>9個點和7個點的數目應該是2倍關系(前7位數定了,后兩位只有兩種排列方式,如果去掉后2位則前7位數有一半重復了)

...

設總方案數為 num,連接 i 個點的方案總數為 n( i ),例如n( 9 ) = 62632

則:1式:num = n( 4 ) + n( 5 ) + n( 6 ) + n( 7 ) + n( 8 ) + n( 9 )

2式:n( 8 ) = n( 9 ), n( 7 ) = n( 9 ) / 2, n( 6 ) = n( 9 ) / 6, n( 5 ) = n( 9 ) / 24, n( 4 ) = n( 9 ) / 120 [規律:n( i ) = n( 9 ) / A(9 - i)(9 - i)]

把2式帶入1式即可求得方案總數,在此不再贅述

(九)總結

探索過程中雖然走了很多彎路,但也有不少收獲,例如動態不合法判斷的BUG是在嘗試方案2時發現的,雖然方案2失敗了,但避免了在方案1中出現類似的問題

只要思路明晰,敢於嘗試,絕對沒有plain try

(十)BUG修正

文中對果殼網算法提出的質疑是錯誤的,原文結果是對的

經過驗證,本文算法存在BUG,信息如下:

當前路徑是 12345687
錯誤原因是下一位 9被錯誤值#39包含
錯誤串為 123456879

BUG分析:出現這個BUG的原因是對自己的算法太過自信,設計算法的時候過分考慮了算法復雜度,省掉了一個不能省的循環(應該先動態修改wrrPos再對結果進行判斷,原算法把二者放在一個循環里了)

現對isValid方法修改如下:

private static boolean isValid(String ans)
	{//判斷ans是否合法
		for(int i = 0;i < ans.length() - 1;i++)
		{
			//獲取當前位置的字符的值
			int currPos = Integer.parseInt(ans.charAt(i) + "");
			//獲取路徑子串
			String path = ans.substring(0, i + 1);
			//獲取錯誤值
			String wrrPos = wrongPos[currPos - 1];
			//獲取可變錯誤值
			if(currPos != 5)//5不可能變
			{
				for(int j = 0;j < wrrPos.length();j++)
				{
					//獲取中間值
					String wrong = wrrPos.charAt(j) + "";
					int mid = (currPos + Integer.parseInt(wrong)) / 2;
					//若中間值已被連接,則錯誤終點可用
					if(path.contains(mid + ""))
						wrrPos = wrrPos.replace(wrong, "#");
				}

				//若下一位是錯誤值則ans不合法
				if(wrrPos.contains(ans.charAt(i + 1) + ""))
					return false;
			}
		}

		return true;
	}

[與原算法唯一的區別是把if判斷語句從循環里拿出來了,當時想法是為了節省一個循環...結果,哎...]

修正后運行結果:

連接9個點共有 140704 種方案

結論:果殼網的結論是正確的!之前對其內部算法的猜測可能有誤,實屬抱歉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM