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();
}
posted @ 2022-03-22 11:27  alfayoung  阅读(929)  评论(0编辑  收藏  举报