双向搜索

双向搜索

定义

双向同时搜索的基本思路是从状态图上的起点和终点同时开始进行 广搜 或 深搜。

如果发现搜索的两端相遇了,那么可以认为是获得了可行解。

过程

双向广搜的步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将开始结点和目标结点加入队列 q
标记开始结点为 1
标记目标结点为 2
while (队列 q 不为空)
{
从 q.front() 扩展出新的 s 个结点

如果 新扩展出的结点已经被其他数字标记过
那么 表示搜索的两端碰撞
那么 循环结束

如果 新的 s 个结点是从开始结点扩展来的
那么 将这个 s 个结点标记为 1 并且入队 q

如果 新的 s 个结点是从目标结点扩展来的
那么 将这个 s 个结点标记为 2 并且入队 q
}

[USACO09NOV] Lights G

题目描述

给出一张 个点 条边的无向图,每个点的初始状态都为

你可以操作任意一个点,操作结束后该点以及所有与该点相邻的点的状态都会改变,由 变成 或由 变成

你需要求出最少的操作次数,使得在所有操作完成之后所有 个点的状态都是

输入格式

第一行两个整数

之后 行,每行两个整数 ,表示在点 之间有一条边。

输出格式

一行一个整数,表示最少需要的操作次数。

本题保证有解。

样例 #1

样例输入 #1

1
2
3
4
5
6
7
5 6 
1 2
1 3
4 2
3 4
2 5
5 3

样例输出 #1

1
3

提示

对于 的数据,。保证没有重边和自环。

题解

  • 考虑贪心,每个开关要么不用要么按一次。
  • 需要用二进制来进行表示哪个状态使用过,哪个没有使用过。
  • 最后是匹配是使用二进制 ^ 匹配前 的状态。
  • 具体实现时,可以把前半段的状态以及达到每种状态的最少按开关次数存储在 map 里面,搜索后半段时,每搜出一种方案,就把它与互补的第一段方案合并来更新答案。
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
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <map>

using namespace std;

int n, m, ans = 0x7fffffff;
map<long long, int> f;
long long a[40];

int main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin >> n >> m;
a[0] = 1;
// 预处理 初始化点1:0001 点2:0010 点3:0100
for(int i = 1; i < n; i ++ ) { // 点1对a0 点2对a1
a[i] = a[i - 1] * 2; // 1 10 100 100 1000
}

for(int i = 1; i <= m; i ++ ) {
int u, v; cin >> u >> v;
--u, --v; //与二进制位匹配 为下一步 例如4就左移3位
a[u] |= ((long long)1 << v); // a |= b →a= a|b
a[v] |= ((long long)1 << u); //压位记录每个点操作影响到的点
}

for (int i = 0; i < (1 << (n / 2)); ++i) { // 对前一半进行搜索 遍历后n\2位置 找按的方法 0是不按 1是按
// 00000 00001 00010 00011 < 00100
long long t = 0; // t表示开始每个点都是灭的
int cnt = 0;
for (int j = 0; j < n / 2; ++j) { // j=0 1 j是看的位数 这里就是看2位
if ((i >> j) & 1) { // i的j位是1 要按这一个点了
t ^= a[j]; // 相同为0,相异为1 出现奇数个为1 偶数个为0 t就是按了之后会变化的点 1代表亮 0代表灭
++cnt; // 操作数++
}
}
if (!f.count(t))
f[t] = cnt;
else
f[t] = min(f[t], cnt); // 这个变化需要的最少操作数
}

for (int i = 0; i < (1 << (n - n / 2)); ++i) { // 对后一半进行搜索 这里的000到111分别表示前三位数 按不按
// 00000 00001 00010 00011 00100 00101 00110 00111 < 01000
long long t = 0;
int cnt = 0;
for (int j = 0; j < (n - n / 2); ++j) { // 这里的j仅仅进行操作动作,也就是按哪些位置 看3位
if ((i >> j) & 1) {
t ^= a[n / 2 + j]; // 按前三位会产生的变化
++cnt;
}
}
// 最终目的是全亮 11111
if (f.count((((long long)1 << n) - 1) ^ t))// 后半段与前半段匹配
ans = min(ans, cnt + f[(((long long)1 << n) - 1) ^ t]);
}
// 这样写也行
// for (int i = 0; i < (1 << (n - n / 2)); ++i) { // 对后一半进行搜索 这里的000到111分别表示前三位数 按不按
// // 00000 00001 00010 00011 00100 00101 00110 00111 < 01000
// long long t = ((long long)1 << n) - 1;
// int cnt = 0;
// for (int j = 0; j < (n - n / 2); ++j) { // 这里的j仅仅进行操作动作,也就是按哪些位置 看3位
// if ((i >> j) & 1) {
// t ^= a[n / 2 + j]; // 按前三位会产生的变化
// ++cnt;
// }
// }
// // 最终目的是全亮 11111
// if (f.count(t))// 后半段与前半段匹配
// ans = min(ans, cnt + f[t]);
// }
cout << ans;

return 0;
}