如何编码状态?
状态
状态是求解过程中的一步。确定状态是什么,如何表示,如何标记,是状态空间搜索中的一环。
首先确定状态是什么,这就需要具体问题具体分析。如何表示状态?状态一般是排列或组合比,如,迷宫最短路中当前所在点 (r, c)
是组合,八数码问题的盘面是排列。状态存储使用数组,以集合 {1, 2, 3, 4}
例,排列 {1, 2, 4, 3}
数组就是 [1, 2, 4, 3]
,组合 {1, 3, 4}
数组就是 [1, 0, 1, 1]
。
其次如何标记,如果 N 元组 N 个坐标组成的空间较小,直接在空间里标记即可,比如二维迷宫最短路,坐标 (r, c)
直接使用 d[r][c]
标记。但是如果个数较多,或是值较大,状态又没有充满整个空间,就会浪费很多存储空间,因此给状态编号,所需的存储空间就是状态总数,开一个不少于状态总数的一维数组即可。
给状态编号就是编码,还原就是解码。
排列(不放回有序抽样)
排列即从备选集合中顺序有关地挑选元素,不可再次挑选,N 元素排列总数为 N!。
如何编码?首先找找规律吧。
排列 | 编码 | 数码 (6, 2, 1, 1) |
---|---|---|
1234 | 0 | 0 0 0 0 |
1243 | 1 | 0 0 1 0 |
1324 | 2 | 0 1 0 0 |
1342 | 3 | 0 1 1 0 |
1423 | 4 | 0 2 0 0 |
1432 | 5 | 0 2 1 0 |
2134 | 6 | 1 0 0 0 |
2143 | 7 | 1 0 1 0 |
2314 | 8 | 1 1 0 0 |
2341 | 9 | 1 1 1 0 |
2413 | 10 | 1 2 0 0 |
2431 | 11 | 1 2 1 0 |
3124 | 12 | 2 0 0 0 |
3142 | 13 | 2 0 1 0 |
3214 | 14 | 2 1 0 0 |
3241 | 15 | 2 1 1 0 |
3412 | 16 | 2 2 0 0 |
3421 | 17 | 2 2 1 0 |
4123 | 18 | 3 0 0 0 |
4132 | 19 | 3 0 1 0 |
4213 | 20 | 3 1 0 0 |
4231 | 21 | 3 1 1 0 |
4312 | 22 | 3 2 0 0 |
4321 | 23 | 3 2 1 0 |
合理猜测,数码代表逆序数,第 n 位权重是 n! 即 n 阶乘,因此编码是逆序数乘权重,解码是将编码解析得到逆序数,再用逆序数取数。
编码
数组 fact
存储阶乘,事先计算好,因为阶乘增长很快,数组不会很大。接下来对每一位求逆序数,逆序数乘权重,加和到编码中。
int encode_permutation(int k)
{
int code = 0;
for (int i = 0; i < k; i++) {
int cnt = 0;
for (int j = i+1; j < k; j++) if (P[i] > P[j]) cnt++;
code += (cnt * fact[k-i-1]);
}
return code;
}
解码
解码需要安置一个元素后将它从备选集合中去除,所以比较麻烦。首先,除权重得到每一位的逆序数,逆序数就是在备选集合中的顺位,循环计数到该元素后退出,取元素标记已挑选。数组 v
记录备选集合元素是否已被挑选,c
用于计数。
void decode_permutation(int k, int code, int *a)
{
int v[MAXN];
memset(v, 0, sizeof(v));
for (int i = 0; i < k; i++) {
int cnt = code / fact[k-i-1];
// select
int j = 0, c = 0;
for (j = 0; j < k; j++) {
if (v[j] == 0) {
if (cnt == c) break;
else c++;
}
}
v[j] = 1;
a[i] = S[j];
code = code % fact[k-i-1];
}
}
组合(不放回无序抽样)
组合即从备选集合顺序无关地挑选元素,不可再次挑选,N 元素的组合总数为 2^N。
组合就是子集,可用位矢量法和二进制法表示。位矢量法就是将元素选中标志存储于一个数组中,一般 1 选中 0 不选中。以集合 s = {1, 2, 3, 4}
为例,子集 sub = {1, 3, 4}
可表示为 [1, 0, 1, 1]
。那么既然元素只有选中、不选中两种,那么二进制足够表示,前例用二进制法表示就是 1011 == 13
(低位在前,不过反着来也没问题)。不难看出,位矢量表示就是状态本身,二进制表示就是状态编号。
编码
二进制表示是编码,位矢量表示转化为编码需按照一定顺序(从低到高或从高到低都行)依次放入 code
二进制位中。C[i]
代表第 i 位的二进制位即选中标志,用 C[i] << i
移至第 i 位,或运算合并到 code
中。
int encode_combination(int n)
{
int code = 0;
for (int i = 0; i < n; i++) {
code = code | (C[i] << i);
}
return code;
}
解码
同理,位矢量表示是状态,二进制表示转化为状态就是将二进制位依次取出存入数组。
void decode_combination(int n, int code, int *a)
{
for (int i = 0; i < n; i++) {
a[i] = code&1;
code = code >> 1;
}
}
允许重复排列(放回有序抽样)
允许重复排列即从备选集合顺序有关地挑选元素但是可再次挑选,由于可以重复,选择元素个数可以不定,总数与选择元素个数有关,N 选 K 允许重复排列总数为 N^K。不难看出,N 选 K 则是 K 位 N 进制,编码、解码就是进制转化。
允许重复组合(放回无序抽样)
允许重复组合即从备选集合顺序无关地挑选元素但是可再次挑选,同理总数与选择元素个数有关,N 选 K 允许重复组合总数为 K^N。状态用位矢量法表示,只不过原先的选中标志变为选中数目。而二进制显然不够,因此使用 K 进制。
问题
八数码问题
盘面是状态,它是 9 位数的排列,通过编码、解码标记状态就不用开 9 维数组了。
Travelling by Stagecoach (POJ 2686)
状态压缩,车票是一个集合(组合),将集合编码为整数,也就是集合的二进制表示,如此一来集合就可以作为数组下标使用。
参考
- 《离散数学及其应用》罗森 著:四种计数问题
- 《算法竞赛入门经典(第 2 版)》刘汝佳 著:编码解码排列,问题来源
- 《概率(第一卷)》施利亚耶夫 著:四种抽样,能和计数问题对应
- 《挑战程序设计竞赛(第 2 版)》秋叶拓哉 岩田阳一 北川宜稔 著:问题来源