Wordle 與 信息論


Wordle 與 信息論

前言

最近,wordle 游戲在朋友圈中大火。那么 wordle 究竟是什么呢?其實就是一個猜單詞游戲,每次輸入一個長度為 \(5\) 的單詞,系統會告訴你,在你的猜測中,哪些字母的位置是正確的,哪些字母出現在目標單詞中但是位置不正確。根據這些提示,你需要在 \(5\) 次機會之內找到這個單詞。

本人在看了 3b1b 的視頻之后,發現 NOIWC2022T3 猜詞恰好正是 wordle 游戲,於是開始嘗試。

原題鏈接:https://www.luogu.com.cn/problem/P8079

視頻鏈接:https://www.bilibili.com/video/BV1zZ4y1k7Jw?share_source=copy_web

如何解決?

首先考慮最朴素的思想,每次在剩余的單詞庫中隨機一個單詞輸入,根據得到的反饋在單詞庫中刪除不合法的單詞,不斷執行這個操作直到結束為止。這個算法好像能夠拿到 \(80\) 分。

朴素的思想有什么瓶頸呢?主要是“隨機”過於隨便了,完全是賭運氣,我們若能夠加入一些啟發式的想法,就能讓這個隨機變的更加有章法可依,從而提高正確率。

要有啟發式的想法,那肯定要引入一個啟發式估價函數。具體地,在本題的條件之下,我們需要在眾多的備選項之中,挑選出能盡可能多地篩除干擾項的那個單詞進行猜測,而不是純隨機的從備選中找一個單詞進行猜測。

怎么用代數化的語言定義“盡可能多地篩除干擾項”呢?這時候就需要引入信息熵的概念了,這個值又稱作為香農熵。如果我們通過一次“篩查”,能夠排除 \(\dfrac 12\) 的干擾項,那么對於占比為 \(P\) 的信息,我們需要 \(I\) 次“篩查”,\(I\) 滿足 \(\left(\dfrac 12\right)^I = P\),化簡得到 \(I= -\log_2 P\),這里的 \(I\) 就稱作為“信息量”,可以發現,信息量越大,能夠篩除的干擾項也就越多。有了信息量的概念,我們就能夠自然地得到信息熵的定義式,我們只要計算信息量的期望就是信息熵:

\[S(X) = -\sum_{x\in \forall outcomes}P(x)\log_2P(x) \]

回到本問題中,我們定義猜測特定單詞的好壞為,對於所有 \(3^5\) 種可能的回饋,計算信息熵。具體地,設 \(cnt[S]\) 表示 \(S\) 回饋后剩下的單詞庫數量,\(T\) 為當前總共單詞庫數量,則有:

\[S(X) = -\sum_{S\in \{3^5\}} \frac{cnt[S]}T \log_2 \frac{cnt[S]}{T} \]

如果這么算的話,會非常慢,計算一個單詞的時間復雜度就為 \(O(3^5 \times 5^2\times T)\),常數有點太大了。

我們重新想想,狀態 \(S\) 和單詞的對應關系,給定猜測的單詞和一個狀態 \(S\),我們或許能夠找到多個匹配的單詞,但如果反過來,給定匹配的單詞和猜測的單詞,我們必定只能找到一個狀態 \(S\)。也就是說,我們剛才暴力枚舉所有狀態和單詞庫的時候有着極大的冗余,我們不妨反過來計算每個單詞庫中的單詞對 \(cnt[S]\)貢獻,這樣計算信息熵的復雜度能夠順利地降為 \(O(5^2 T)\)

這樣下來,利用信息熵的啟發性優化,我們能夠拿到 \(92\) 分。這部分是第一天寫的,當我第二天回過頭來看時,發現只拿 \(92\) 分是有原因的。。。我們考慮一件事情,我們當前最佳的猜測單詞不一定非要從篩選下來的單詞庫中去找,比如我們已經肯定了某個位置是 \(a\) 了,我們不一定下次這個位置要填 \(a\),也可以填其他字母去試其他的位置,這樣做獲得的信息熵或許會更高。所以,每次搜索應當在全局單詞庫中搜索最大信息熵。然而,這樣做還是不夠優秀,當我們搜索范圍擴大到全局時,我們的程序在篩選后的單詞庫中進行猜測的概率會相應減少,換言之,就是過於注重更大程度地篩除干擾項,而變得不太敢猜測了。這種情況其實比較好解決,只需要給篩選過的單詞庫中的熵加權即可,即給一個激勵。顯然,當篩選后的單詞庫越小,我們就更應該激勵程序去大膽猜想。於是,我定義的激勵函數為 \(\tanh (\dfrac 1{\log_2 T})\),這里我們使用激活函數 \(\tanh\)\((0, +\infty)\) 的值進行一波平滑處理。

本題出現的單詞是隨機挑選的,沒有使用的頻率等特征。3b1b 視頻中給出了這樣一個優化,如果不同單詞的使用頻率不同,我們就能夠給每個單詞加上一個權重,即我們對 \(cnt[S]\) 算貢獻時,不是單純的加 \(1\) 了,而是加上每個單詞的權值,這個權值可大可小,你大可設一個單詞的權值為 \(10\),而另一個不常用單詞的權值為 \(0.001\)。與此同時,我們也可以嘗試定義期望猜測次數來作為估價函數,表達式為(文字表述)當前猜的詞成為答案的概率 乘以 當前步數 加上 一減去上述概率 乘以 處理當前大小的信息量期望多少步。

反思與總結

這種做法可以勉強過題,但是實現還不夠精細。之后或許能夠使用決策樹等操作進一步優化,如果算力足夠,還可以預測兩步之后的信息熵,這樣的決策一定更加精確。

wordle 游戲讓我能夠稍微入門一些信息論?寫完代碼還是挺有成就感的。代碼如下:

展開查看代碼
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double db;
vector<string> rem, rem2, trem, irem;
const char *bstInitial[26] = {"slier", "lares", "lares", "tores", "tarns", "arles", "lares", "lares", "snare", "ousel", "ranis", "nares", "tares", "aides", "tries", "lares", "raise", "aides", "plate", "nares", "snare", "riles", "nares", "cones", "kanes", "aeons"};

int cnt[245], gold[5], silver[5], buc[30];
db shannonEntropy(string guess) {
	db ret = 0; memset(cnt, 0, sizeof(cnt));
	for (int i = 0; i < rem.size(); i++) {
		for (int j = 0; j < 5; j++) gold[j] = silver[j] = 0;
		for (int j = 0; j < 5; j++) gold[j] = (rem[i][j] == guess[j]);
		for (int j = 0; j < 5; j++) if (!gold[j])
			for (int k = 0; k < 5; k++) if (j != k && !gold[k])
				silver[j] |= (guess[j] == rem[i][k]);
		int S = 0;
		for (int j = 0, bs = 1; j < 5; j++, bs *= 3) {
			if (gold[j]) S += 2 * bs;
			else if (silver[j]) S += 1 * bs;
		}
		cnt[S]++;
	}
	for (int S = 0; S < 243; S++) {
		if (!cnt[S]) continue;
		db P = (db)cnt[S] / rem.size();
		ret = ret - P * log2(P);
	}
	return ret;
}

void init(int num_scramble, const char *scramble) {
	for (int i = 0; i < num_scramble; i++) {
		string tmps; tmps.resize(5);
		for (int j = 0; j < 5; j++) tmps[j] = scramble[i * 5 + j];
		rem.push_back(tmps); irem.push_back(tmps);
	}
}
string lstGuess;
const char *guess(int num_testcase, int remaining_guesses, char initial_letter, bool *gold, bool *silver) {
	if (remaining_guesses == 5) {
		rem.clear(); rem2.clear();
		for (int i = 0; i < irem.size(); i++) {
			if (irem[i][0] == initial_letter) rem.push_back(irem[i]);
			else rem2.push_back(irem[i]);
		}
		lstGuess = bstInitial[initial_letter - 'a'];
		return lstGuess.c_str();
	} else {
		for (int i = 0; i < rem.size(); i++) {
			int flg = 1;
			for (int j = 0; j < 5; j++) if (gold[j] && rem[i][j] != lstGuess[j]) flg = 0;
			for (int j = 0; j < 5; j++) if (silver[j] && rem[i][j] == lstGuess[j]) flg = 0;
			for (int j = 0; j < 5; j++) if (silver[j]) {
				int flg2 = 0;
				for (int k = 0; k < 5; k++) if (!gold[k] && lstGuess[j] == rem[i][k]) flg2 = 1;
				flg &= flg2;
			}
			for (int j = 0; j < 5; j++) if (!gold[j] && !silver[j])
				for (int k = 0; k < 5; k++) if (!gold[k] && rem[i][k] == lstGuess[j])
					flg = 0;
			if (flg) trem.push_back(rem[i]);
		}
		swap(rem, trem); trem.clear();
	}
	//cout << "rem ========" << endl;
	//for (int i = 0; i < rem.size(); i++) cout << rem[i] << endl;
	//cout << "========= rem" << endl;
	db mx1 = -1, mx2 = -1; string bstGuess1, bstGuess2;
	for (int i = 0; i < rem.size(); i++) {
		db tmp = shannonEntropy(rem[i]);
		if (tmp > mx1) mx1 = tmp, bstGuess1 = rem[i];
	}
	mx1 = mx1 + 2 * tanh(1 / log2(rem.size()));
	//cout << 2 * tanh(1 / log2(rem.size())) << endl;
	for (int i = 0; i < rem2.size(); i++) {
		db tmp = shannonEntropy(rem2[i]);
		if (tmp > mx2) mx2 = tmp, bstGuess2 = rem2[i];
	}
	if (mx1 >= mx2) lstGuess = bstGuess1;
	else lstGuess = bstGuess2;
	return lstGuess.c_str();
}


免責聲明!

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



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