一个人要将一匹狼、一只羊、一筐菜运到河对岸,但是他的船太小了,一次只能带一样过河。当他不在时,狼要吃羊、羊要吃菜。他怎样才能安全地将狼、羊、菜都运过河呢?

图示

  请编写程序,找出过河的全部方案。

  用 4 个字母分别表示人、狼、羊、菜的状态:

  • 用 M 表示人在,用 . 表示人不在
  • 用 W 表示狼在,用 . 表示狼不在
  • 用 G 表示羊在,用 . 表示羊不在
  • 用 C 表示菜在,用 . 表示菜不在
  • 用 -> 表示船将从左岸去右岸,用 <- 表示船将从右岸去左岸。

  举例说明:

  若人狼羊菜全在左岸,则状态表示为:

1
MWGC -> ....

  若人将羊运到右岸,左岸只剩狼和菜,则状态表示为:

1
.W.C <- M.G.

  注:为使输出的解法排列顺序一致,要求每次渡河时都按以下顺序进行探索:

  1. 直接过河(不带任何东西)
  2. 带狼过河
  3. 带羊过河
  4. 带菜过河

输入格式

初始状态
终止状态

输出格式

  若有解,则输出所有解法。每一个解法由若干行组成,每行表示一个状态。不同的解法之间空一行。

  若无解,则输出None

题解

  这道题很明显是一个搜索 + 模拟题,问题的关键就在于如何清晰的将代码表示出来。

  首先要考虑如何存储状态,由于状态左右是一定相反的,所以我们能通过一半的状态推出完整的状态,因此我们使用一个长度为 4 的数组配合一个布尔类型的量就可以完成的存储状态。不过为了编码方便,这里我们还是使用一个长度为 8 的数组和一个布尔类型变量来存储状态。同时我们规定:当布尔值为真时表示箭头向右,否则向左。

  下文中我们需要使用一半的状态推算另一半的状态,所以我们需要先存储一下每个位置对应的符号(本文中所有片段代码仅给出kotlin版本,其它语言只提供完整代码):

1
2
3
4
5
6
7
8
9
10
11
/** 存储输入的站位  */
val rdm = CharArray(4)

fun main() {
// do something
for (i in 0 until 4) {
// 这里的 map 是输入的起始状态
rdm[i] = if (map[i] == '.') map[i + 4] else map[i]
}
// do something
}

  然后确定我们用什么方法进行搜索,显然是 DFS,如果使用 BFS,还需要使用多个集合保存路径数据,空间和时间成本都比 DFS 要高。

  接下来再考虑剪枝的问题,在进行 DFS 搜索的过程中必然是需要剪枝的,遇到已经经过的状态是需要跳过的,不然无法得到正确答案,也非常容易超时。

  有没有办法以尽可能短的时间进行状态查重呢?当然是有的,我们可以对状态进行哈希处理,把状态映射到整型范围内,就可以以O(1)的时间复杂度进行状态查重。

  下面我们列出哈希化状态的代码:

1
2
3
4
5
6
7
8
9
val codeMap = charArrayOf('M', 'W', 'G', 'C', '.')

fun CharArray.getCode(direction: Boolean): Int {
var result = if (direction) 1 else 0
for (i in 0 until 4) {
result = (result shl 3) or codeMap.findIndex(this[i], codeMap.indices)
}
return result
}

  在上面的代码中,我们在进行哈希化时只处理了一半的状态。我们给每个位置分配了 3bit,第 13 位用来存储箭头方向,总共使用了 13bit,所以可知哈系数的范围在[0, 213)内(其实严格说应该是在[668, 6436]内)。

  同时,我们还需要提供一个反哈希的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun parseCode(code: Int): String {
// 注意使用循环拼接字符串时 StringBuilder 优先于直接使用加号
// 如果是使用类似于 `str = 'a' + i + 'b' + q` 的写法的话,两者性能没有区别
val sb = StringBuilder(13)
val parse = intArrayOf(
(code shr 9) and 7,
(code shr 6) and 7,
(code shr 3) and 7,
code and 7
)
parse.forEach { sb.append(codeMap[it]) }
sb.append(if (code shr 12 == 0) " <- " else " -> ")
parse.forEachIndexed { i, value ->
sb.append(if (value == 0b100) rdm[i] else '.')
}
sb.append('\n')
return sb.toString()
}

  现在我们处理好了判重的问题,那么剩下的就是非常简单的 DFS 了,直接给出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/** 存储输入的站位  */
val rdm = CharArray(4)
/** 存储目标序列 */
val dist = CharArray(8)
/** 存储目标序列的方向 */
var distDirection = false
/** 存储当前序列 */
val map = CharArray(8)
/** 存储当前序列的方向 */
var direction = false
/** 目标序列的 code */
var distCode = 0

fun main() {
direction = read(map)
distDirection = read(dist)
distCode = dist.getCode(distDirection)
for (i in 0 until 4) {
rdm[i] = if (map[i] == '.') map[i + 4] else map[i]
}
if (!dfs())
print("None")
}

/** 移动顺序 */
val moveOrder = charArrayOf('W', 'G', 'C')
/** 标记是否为第一次找到答案 */
var nonFirst = false
/** 标记指定序列是否访问过 */
val record = BooleanArray(1 shl 13)
/** 存储路径 */
val list = LinkedList<Int>()

fun dfs(): Boolean {
val code = map.getCode(direction)
if (record[code] || !check()) return false
list += code
// 检查当前状态是否和目标状态相同
if (code == distCode) {
// 如果初始状态和目标状态相同,则直接返回 false
if (list.size == 1) return false
if (nonFirst) println()
else nonFirst = true
list.forEach { print(parseCode(it)) }
list.removeLast()
return true
}
record[code] = true
val offset = if (direction) 4 else -4
direction = !direction
// 先尝试人单独过河
val mIndex = findIndex('M', offset)
map.swap(mIndex, mIndex + offset)
var result = dfs()
if (!result) {
// 一次尝试人带一个过河
for (value in moveOrder) {
val index = findIndex(value, offset)
if (index != -1) {
map.swap(index, index + offset)
if (dfs()) result = true
map.swap(index, index + offset)
}
}
}
map.swap(mIndex, mIndex + offset)
direction = !direction
record[code] = false
list.removeLast()
return result
}

fun check(): Boolean {
val offset = if (direction) -4 else 4
val w = findIndex('W', offset)
val g = findIndex('G', offset)
if (w != -1 && g != -1) return false
val c = findIndex('C', offset)
return g == -1 || c == -1
}

/** 在指定范围内查找指定元素的下标 */
fun CharArray.findIndex(dist: Char, range: IntRange): Int {
for (i in range) {
if (this[i] == dist) return i
}
return -1
}

/**
* 在指定范围的序列中查找目标下标
* @param dist 目标
* @param offset 4 表示在 [0, 4) 范围内查找,-4 表示在 [4, 8) 范围内查找
* @return 查找到返回下标,否则返回 -1
*/

fun findIndex(dist: Char, offset: Int): Int {
val left = if (offset == 4) 0 else 4
return map.findIndex(dist, left until left + 4)
}

/** 映射表 */
val codeMap = charArrayOf('M', 'W', 'G', 'C', '.')

/** 将 code 转换为字符串 */
fun parseCode(code: Int): String {
val sb = StringBuilder(14)
val parse = intArrayOf(
(code shr 9) and 7,
(code shr 6) and 7,
(code shr 3) and 7,
code and 7
)
parse.forEach { sb.append(codeMap[it]) }
sb.append(if (code shr 12 == 0) " <- " else " -> ")
parse.forEachIndexed { i, value -> sb.append(if (value == 0b100) rdm[i] else '.') }
sb.append('\n')
return sb.toString()
}

/** 将指定状态哈希化 */
fun CharArray.getCode(direction: Boolean): Int {
var result = if (direction) 1 else 0
for (i in 0 until 4) {
result = (result shl 3) or codeMap.findIndex(this[i], codeMap.indices)
}
return result
}

/** 交换指定值 */
fun CharArray.swap(a: Int, b: Int) {
this[a] = this[b].also { this[b] = this[a] }
}

/**
* 读取一个状态
* @return 方向,true 向右,false 向左
*/

fun read(map: CharArray): Boolean =
with(System.`in`) {
for (i in 0 until 4) map[i] = read().toChar()
read()
val direction = read() == '-'.code
skip(2)
for (i in 4 until 8) map[i] = read().toChar()
read()
direction
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
public class Main {

/** 存储输入的站位 */
static char[] rdm = new char[4];
/** 存储目标序列 */
static char[] dist = new char[8];
/** 存储目标序列的方向 */
static boolean distDirection;
/** 存储当前序列 */
static char[] map = new char[8];
/** 存储当前序列的方向 */
static boolean direction;
/** 目标序列的 code */
static int distCode;

public static void main(String[] args) throws IOException {
direction = read(map);
distDirection = read(dist);
distCode = getCode(dist, distDirection);
for (int i = 0; i != 4; ++i) {
rdm[i] = map[i] == '.' ? map[i + 4] : map[i];
}
if (!dfs())
System.out.print("None");
}

/** 移动顺序 */
static char[] moveOrder = {'W', 'G', 'C'};
/** 标记是否为第一次找到答案 */
static boolean nonFirst = false;
/** 标记指定序列是否访问过 */
static boolean[] record = new boolean[8200];
/** 存储路径 */
static Deque<Integer> list = new LinkedList<>();

static boolean dfs() {
int code = getCode(map, direction);
if (record[code] || !check()) return false;
list.add(code);
// 检查当前状态是否和目标状态相同
if (code == distCode) {
// 如果初始状态和目标状态相同,则直接返回 false
if (list.size() == 1) return false;
if (nonFirst) System.out.println();
else nonFirst = true;
for (int history : list) {
System.out.print(parseCode(history));
}
list.removeLast();
return true;
}
record[code] = true;
int offset = direction ? 4 : -4;
direction = !direction;
boolean result = false;
// 先尝试人单独过河
int mIndex = findIndex('M', offset);
map[mIndex] = '.';
map[mIndex + offset] = 'M';
if (dfs()) result = true;
else {
// 依次尝试人带一个过河
for (char value : moveOrder) {
int index = findIndex(value, offset);
if (index != -1) {
map[index] = '.';
map[index + offset] = value;
if (dfs()) result = true;
map[index] = value;
map[index + offset] = '.';
}
}
}
map[mIndex] = 'M';
map[mIndex + offset] = '.';
direction = !direction;
record[code] = false;
list.removeLast();
return result;
}

/** 检查当前序列是否合法 */
static boolean check() {
int offset = direction ? -4 : 4;
int w = findIndex('W', offset);
int g = findIndex('G', offset);
if (w != -1 && g != -1) return false;
int c = findIndex('C', offset);
return g == -1 || c == -1;
}

/** 在指定范围内查找下标 */
static int findIndex(char[] array, char dist, int left, int right) {
for (int i = left; i != right; ++i) {
if (array[i] == dist) return i;
}
return -1;
}

/**
* 在指定范围内查找目标下标
* @param dist 目标
* @param offset 4 表示在 [0, 4) 范围内查找,-4 表示在 [4, 8) 范围内查找
* @return 查找到返回下标,否则返回 -1
*/

static int findIndex(char dist, int offset) {
int left = offset == 4 ? 0 : 4;
return findIndex(map, dist, left, left + 4);
}

// 映射表
static char[] codeMap = {'M', 'W', 'G', 'C', '.'};

/** 将 code 转换为字符串 */
static String parseCode(int code) {
StringBuilder sb = new StringBuilder(14);
int[] parse = {
(code >> 9) & 7,
(code >> 6) & 7,
(code >> 3) & 7,
code & 7
};
for (int value : parse) {
sb.append(codeMap[value]);
}
sb.append((code >> 12) == 0 ? " <- " : " -> ");
for (int i = 0; i != parse.length; ++i) {
sb.append(parse[i] == 0b100 ? rdm[i] : '.');
}
sb.append("\n");
return sb.toString();
}

/** 将指定状态哈希化 */
static int getCode(char[] map, boolean direction) {
int result = direction ? 1 : 0;
for (int i = 0; i != 4; ++i) {
result = (result << 3) | findIndex(codeMap, map[i], 0, codeMap.length);
}
return result;
}

/**
* 读取一个状态
* @return 方向,true 向右,false 向左
*/

static boolean read(char[] map) throws IOException {
for (int i = 0; i != 4; ++i) map[i] = (char) System.in.read();
System.in.read();
boolean direction = System.in.read() == '-';
System.in.skip(2);
for (int i = 4; i != 8; ++i) map[i] = (char) System.in.read();
System.in.read();
return direction;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using std::swap;
using List = std::list<int>;

// 输入时的站位
char rdm[4];
// 目标序列
char dist[8];
// 目标序列的方向
bool distDirection;
// 当前序列
char map[8];
// 当前序列的方向
bool direction;
// 目标序列的 code
int distCode;
// 移动顺序
char moveOrder[] = {'W', 'G', 'C'};
// 标记是否为第一次找到答案
bool nonFirst = false;
// 标记指定序列是否访问过
bool record[8200];
// 存储路径
List list;

// 在指定范围内搜索目标下标
int findIndex(char array[], char dist, int left, int right) {
for (int i = left; i != right; ++i) {
if (array[i] == dist) return i;
}
return -1;
}

// 在指定范围内查找目标下标
int findIndex(char dist, int offset) {
int left = offset == 4 ? 0 : 4;
return findIndex(map, dist, left, left + 4);
}
// 检查当前序列是否合法
bool check() {
int offset = direction ? -4 : 4;
int w = findIndex('W', offset);
int g = findIndex('G', offset);
if (w != -1 && g != -1) return false;
int c = findIndex('C',offset);
return g == -1 || c == -1;
}

// 映射表
char codeMap[] = {'M', 'W', 'G', 'C', '.'};

// 将指定状态哈希化
int getCode(const char map[], bool direction) {
int result = direction ? 1 : 0;
for (int i = 0; i != 4; ++i) {
result = (result << 3) | findIndex(codeMap, map[i], 0, 5);
}
return result;
}

// 将 code 转化为字符串并打印
void printCode(int code) {
int parse[] = {
(code >> 9) & 7,
(code >> 6) & 7,
(code >> 3) & 7,
code & 7
};
for (const auto& value : parse) {
printf("%c", codeMap[value]);
}
printf(" %s ", (code >> 12) == 0 ? "<-" : "->");
for (int i = 0; i != 4; ++i) {
printf("%c", parse[i] == 0b100 ? rdm[i] : '.');
}
printf("\n");
}

bool dfs() {
int code = getCode(map, direction);
if (record[code] || !check()) return false;
list.push_back(code);
// 检查当前状态是否和目标状态相同
if (code == distCode) {
// 如果初始状态和目标状态相同,则直接返回 false
if (list.size() == 1) return false;
if (nonFirst) printf("\n");
else nonFirst = true;
for (const auto& history : list) {
printCode(history);
}
list.pop_back();
return true;
}
record[code] = true;
int offset = direction ? 4 : -4;
direction = !direction;
bool result = false;
// 先尝试人单独过河
int mIndex = findIndex('M', offset);
swap(map[mIndex], map[mIndex + offset]);
if (dfs()) result = true;
else {
// 依次尝试人带一个过河
for (const auto& value : moveOrder) {
int index = findIndex(value, offset);
if (index != -1) {
swap(map[index], map[index + offset]);
if (dfs()) result = true;
swap(map[index], map[index + offset]);
}
}
}
swap(map[mIndex], map[mIndex + offset]);
direction = !direction;
record[code] = false;
list.pop_back();
return result;
}

int read(char map[]) {
for (int i = 0; i != 4; ++i) scanf("%c", map + i);
getchar();
bool direction = getchar() == '-';
scanf("%*c%*c");
for (int i = 4; i != 8; ++i) scanf("%c", map + i);
getchar();
return direction;
}

int main() {
direction = read(map);
distDirection = read(dist);
distCode = getCode(dist, distDirection);
for (int i = 0; i != 4; ++i) {
rdm[i] = map[i] == '.' ? map[i + 4] : map[i];
}
if (!dfs())
printf("None");
return 0;
}

  上文我们说到,可以仅存储一半的状态,下面我们就用kotlin实现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import java.util.*

/** 映射表 */
val codeMap = charArrayOf('M', 'W', 'G', 'C', '.')
/** 存储输入的站位 */
var rdm = State(0)
/** 存储当前序列 */
var map = read().let {
val tmp = State(it)
for (i in 0 until 4) {
val dist = tmp[i]
rdm = rdm.set(i, if (dist == '.') tmp[i + 4] else dist)
}
it.zipCode()
}
/** 存储目标序列 */
val dist = read().zipCode()

fun main() {
if (!dfs())
print("None")
}

/** 移动顺序 */
var moveOrder = charArrayOf('W', 'G', 'C')
/** 标记是否为第一次找到答案 */
var nonFirst = false
/** 标记指定序列是否访问过 */
var record = BooleanArray(8200)
/** 存储路径 */
var list = LinkedList<State>()

fun dfs(): Boolean {
if (record[map.value] || !map.legal) return false
list += map
// 检查当前状态是否和目标状态相同
if (map == dist) {
// 如果初始状态和目标状态相同,则直接返回 false
if (list.size == 1) return false
if (nonFirst) println()
else nonFirst = true
list.forEach { print(it.toString()) }
list.removeLast()
return true
}
record[map.value] = true
map = map.flipDirection()
// 先尝试人单独过河
val mIndex = map.find('M')
map = map.swap(mIndex)
var result = dfs()
if (!result) {
// 一次尝试人带一个过河
for (value in moveOrder) {
val index = map.find(value)
if (index != -1) {
map = map.swap(index)
if (dfs()) result = true
map = map.swap(index)
}
}
}
map = map.swap(mIndex).flipDirection()
record[map.value] = false
list.removeLast()
return result
}

/** 保存状态 */
@JvmInline
value class State(val value: Int) {

/** 获取方向 */
private val direction: Boolean
get() = value shr 12 == 1

/** 是否合法 */
val legal: Boolean
get() {
val w = find('W')
val g = find('G')
if (w != -1 && g != -1) return false
return g == -1 || find('C') == -1
}

/** 查找指定符号的位置 */
fun find(dist: Char): Int {
for (i in 0 until 4) {
if (this[i, !direction] == dist) return i
}
return -1
}

/** 翻转方向 */
fun flipDirection(): State = State(
if (direction) value and 1.shl(12).inv() else value or 1.shl(12)
)

/** 交换指定值 */
fun swap(count: Int): State = set(count, getOpposite(count))

/**
* 获取指定位置或其对称位置上的符号
* @param count 下标(从右往左,从 0 开始递增)
* @param notFlip 是否不取对称位置
*/

operator fun get(count: Int, notFlip: Boolean): Char =
if (notFlip) this[count] else getOpposite(count)

/**
* 获取指定位上的符号
* @param count 下标(从右往左,从 0 开始递增)
*/

operator fun get(count: Int): Char = codeMap[(value shr (count * 3)) and 0b111]

/**
* 获取与指定位置对称的位置上的符号
* @param count 下标(从右往左,从 0 开始递增)
*/

private fun getOpposite(count: Int): Char {
val dist = this[count]
return if (dist == '.') rdm[count] else '.'
}

/**
* 设置指定位置上的符号
* @param count 下标(从右往左,从 0 开始递增)
* @param dist 要设置的符号
*/

fun set(count: Int, dist: Char): State {
val code = codeMap.indexOf(dist)
val base = count * 3
var result = value
for (i in 0 until 3) {
val bit = (code shr i) and 1
val tmp = 1.shl(base + i)
result = if (bit == 0) result and tmp.inv() else result or tmp
}
return State(result)
}

override fun toString(): String {
val sb = StringBuilder(14)
val parse = charArrayOf(this[3], this[2], this[1], this[0])
parse.forEach { sb.append(it) }
sb.append(if (direction) " -> " else " <- ")
for (i in 3 downTo 0) sb.append(getOpposite(i))
sb.append('\n')
return sb.toString()
}

}

/** 将状态码压缩到 13bit,存储左侧数据 */
fun Int.zipCode(): State = State((this.shr(12) and 0xFFF) or (this shr 24).shl(12))

/**
* 读取一个状态
* @return 第 25 位表示方向 1 为右,0 为左,然后从最高位到最低位依次存储输入,一位输入占 3bit
*/

fun read(): Int =
with(System.`in`) {
var result = 0
for (i in 0 until 4) {
result = (result shl 3) or codeMap.indexOf(read().toChar())
}
read()
val direction = read() == '-'.code
skip(2)
for (i in 0 until 4) {
result = (result shl 3) or codeMap.indexOf(read().toChar())
}
read()
if (direction) result = result or (1 shl 24)
result
}