Time Limit: 1 secs, Memory Limit: 32 MB , Special Judge
魔板由8个大小相同方块组成,分别用涂上不同颜色,用1到8的数字表示。
其初始状态是
1 2 3 4
8 7 6 5
对魔板可进行三种基本操作:
A操作(上下行互换):
8 7 6 5
1 2 3 4
B操作(每次以行循环右移一个):
4 1 2 3
5 8 7 6
C操作(中间四小块顺时针转一格):
1 7 2 4
8 6 3 5
用上述三种基本操作,可将任一种状态装换成另一种状态。
输入包括多个要求解的魔板,每个魔板用三行描述。
第一行步数N(不超过10的整数),表示最多容许的步数。
第二、第三行表示目标状态,按照魔板的形状,颜色用1到8的表示。
当N等于-1的时候,表示输入结束。
对于每一个要求解的魔板,输出一行。
首先是一个整数M,表示你找到解答所需要的步数。接着若干个空格之后,从第一步开始按顺序给出M步操作(每一步是A、B或C),相邻两个操作之间没有任何空格。
注意:如果不能达到,则M输出-1即可。
4 5 8 7 6 4 1 2 3 3 8 7 6 5 1 2 3 4 -1
2 AB 1 A 评分:M超过N或者给出的操作不正确均不能得分。
ZSUACM Team Member
此题难度不大,重要的是怎么优化,更快效率,更少内存使用。
一开始看到这题,典型的搜索题目。
没多想写出如下平凡的BFS搜索代码:
1 #include <cstdio> 2 #include <string> 3 #include <iostream> 4 #include <set> 5 #include <queue> 6 7 int n, destination, top, bottom, a, b, c, d; 8 std::string ans; 9 std::set<int> visited; 10 11 struct State { 12 int state; 13 std::string step; 14 State(int state, std::string step) { 15 this->state = state; 16 this->step = step; 17 } 18 }; 19 20 int OP_A(int state) { 21 return state / 10000 + state % 10000 * 10000; 22 } 23 24 int OP_B(int state) { 25 top = state / 10000; 26 bottom = state % 10000; 27 top = top % 10 * 1000 + top / 10; 28 bottom = bottom % 10 * 1000 + bottom / 10; 29 return top * 10000 + bottom; 30 } 31 32 int OP_C(int state) { 33 a = state / 1000000 % 10; 34 b = state / 100000 % 10; 35 c = state / 100 % 10; 36 d = state / 10 % 10; 37 38 state -= a * 1000000; 39 state -= b * 100000; 40 state -= c * 100; 41 state -= d * 10; 42 43 state += c * 1000000; 44 state += a * 100000; 45 state += d * 100; 46 state += b * 10; 47 48 return state; 49 } 50 51 void bfs() { 52 int state = 12348765; 53 if (state == destination) { 54 ans = ""; 55 return; 56 } 57 58 std::queue<State> q; 59 q.push(State(state, "")); 60 while (!q.empty()) { 61 State s = q.front(); 62 q.pop(); 63 64 if (visited.find(s.state) != visited.end()) 65 continue; 66 if (s.step.length() > (size_t)n) 67 break; 68 if (s.state == destination) { 69 ans = s.step; 70 break; 71 } 72 73 visited.insert(s.state); 74 75 q.push(State(OP_A(s.state), s.step + "A")); 76 q.push(State(OP_B(s.state), s.step + "B")); 77 q.push(State(OP_C(s.state), s.step + "C")); 78 } 79 } 80 81 int main() { 82 while (scanf("%d", &n) && n != -1) { 83 84 ans = "NOT FOUND"; 85 destination = 0; 86 visited.clear(); 87 88 int temp[8], base = 1, i; 89 for (i = 0; i < 8; i++) 90 scanf("%d", temp + i); 91 for (i = 7; i >= 0; i--) 92 destination += temp[i] * base, base *= 10; 93 94 bfs(); 95 96 if (ans == "NOT FOUND") 97 printf("-1\n"); 98 else 99 std::cout << ans.length() << " " << ans << std::endl; 100 } 101 return 0; 102 }
轻轻松松过了,时间0.4s,内存2300kb。
时间明显慢的不能忍,看排行榜少数人最佳的是0.00s!
于是开始我们的优化过程:
首先联想到,这里用的set数据结构来记录搜索中访问过的节点,而我们对set的使用基本不会涉及交集并集的操作,只是单纯的访问这个元素在不在。
那么有个基础常识需要注意:STL中set是用红黑树实现的,此时对元素的查找是远不如哈希快的,所以我们把set替换成hash_set将会大幅提高效率。
即把原来的set声明改成hash_set即可:
1 __gnu_cxx::hash_set<int> visited;
简单的改动, 时间0.25s,内存2428kb。
还是不够快!
于是继续修改,舍弃hash_set,改为visited数组线性访问,但是内存会爆! 8^8的空间!
用康托展开减少记录空间,即8^8空间压到8!空间大小,此时内存不会爆。
代码如下:
1 #include <cstdio> 2 #include <string> 3 #include <iostream> 4 #include <queue> 5 #include <cstring> 6 7 int n, destination, top, bottom, a, b, c, d, base, i, j; 8 std::string ans; 9 int fact[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320}; 10 bool visited[40321]; 11 12 struct State { 13 int state; 14 std::string step; 15 State(int state, std::string step) { 16 this->state = state; 17 this->step = step; 18 } 19 }; 20 21 int encode(int n) 22 { 23 static int tmp[8]; 24 for (i = 7; i >= 0; i--) { 25 tmp[i] = n % 10; 26 n /= 10; 27 } 28 29 for (i = 0; i < 8; i++) { 30 base = 0; 31 for (j = i + 1; j < 8; j++) 32 if (tmp[i] > tmp[j]) base++; 33 n += fact[7 - i] * base; 34 } 35 36 return n; 37 } 38 39 int OP_A(int state) { 40 return state / 10000 + state % 10000 * 10000; 41 } 42 43 int OP_B(int state) { 44 top = state / 10000; 45 bottom = state % 10000; 46 top = top % 10 * 1000 + top / 10; 47 bottom = bottom % 10 * 1000 + bottom / 10; 48 return top * 10000 + bottom; 49 } 50 51 int OP_C(int state) { 52 a = state / 1000000 % 10; 53 b = state / 100000 % 10; 54 c = state / 100 % 10; 55 d = state / 10 % 10; 56 57 state -= a * 1000000; 58 state -= b * 100000; 59 state -= c * 100; 60 state -= d * 10; 61 62 state += c * 1000000; 63 state += a * 100000; 64 state += d * 100; 65 state += b * 10; 66 67 return state; 68 } 69 70 void bfs() { 71 int state = 12348765; 72 if (state == destination) { 73 ans = ""; 74 return; 75 } 76 77 std::queue<State> q; 78 q.push(State(state, "")); 79 while (!q.empty()) { 80 State s = q.front(); 81 q.pop(); 82 83 if (visited[encode(s.state)]) 84 continue; 85 if (s.step.length() > (size_t)n) 86 break; 87 if (s.state == destination) { 88 ans = s.step; 89 break; 90 } 91 92 visited[encode(s.state)] = true; 93 94 q.push(State(OP_A(s.state), s.step + "A")); 95 q.push(State(OP_B(s.state), s.step + "B")); 96 q.push(State(OP_C(s.state), s.step + "C")); 97 } 98 } 99 100 int main() { 101 while (scanf("%d", &n) && n != -1) { 102 103 ans = "NOT FOUND"; 104 destination = 0; 105 memset(visited, 0, sizeof(visited)); 106 107 int temp[8], base = 1, i; 108 for (i = 0; i < 8; i++) 109 scanf("%d", temp + i); 110 for (i = 7; i >= 0; i--) 111 destination += temp[i] * base, base *= 10; 112 113 bfs(); 114 115 if (ans == "NOT FOUND") 116 printf("-1\n"); 117 else 118 std::cout << ans.length() << " " << ans << std::endl; 119 } 120 return 0; 121 }
时间0.22s,空间1404kb。
效果并没有比hash_set快多少!
那么问题出在哪里呢?
博主想了一天(边上课边想),想到答案:
每个样例都在搜索同一棵树!!!!!
这样会产生大量的重复搜索,在不同的样例之间,特别是testcase很多的情况下,这是多余的。
所以,我们要把整棵树记录下来。
再看我们的数据结构,之前用的整数来存储一个节点状态,但是这里操作ABC效率是低下的,我们不如用3位来存储一个数字,这样ABC操作直接用位运算来完成。
然后只遍历一次搜索树,把每个节点对应的操作记录下来,然后在具体需要的时候回溯逆向输出就是结果,这里考虑到用数组来记录节点状态的话内存开销太大1<<24的大小不是个小数字,所以用的是hash_map来记录树。
代码如下:
1 #include <cstdio> 2 #include <queue> 3 #include <hash_map> 4 5 inline int OP_A(int &n) { 6 return (n & 4095) << 12 | n >> 12; 7 } 8 9 inline int OP_B(int &n) { 10 return (( 7 << 9 | 7 << 21 ) & n) >> 9 | (~(7 << 9 | 7 << 21) & n) << 3; 11 } 12 13 inline int OP_C(int &n) { 14 return ((7 | 7 << 9 | 7 << 12 | 7 << 21) & n) | ((7 << 3) & n) << 3 | ((7 << 6) & n) << 12 | ((7 << 15) & n) >> 12 | ((7 << 18) & n) >> 3; 15 } 16 17 inline int resume_B(int &n) { 18 return ((7 | 7 << 12) & n) << 9 | (~(7 | 7 << 12) & n) >> 3; 19 } 20 21 inline int resume_C(int &n) { 22 return ((7 | 7 << 9 | 7 << 12 | 7 << 21) & n) | ((7 << 3) & n) << 12 | ((7 << 6) & n) >> 3 | ((7 << 15) & n) << 3 | ((7 << 18) & n) >> 12; 23 } 24 25 inline int zip(int *a) { 26 static int code; 27 code = 0; 28 for (int i = 0; i < 8; ++i) 29 code |= (a[i] - 1) << (3 * i); 30 return code; 31 } 32 33 int a[] = {1, 2, 3, 4, 8, 7, 6, 5}, origin = zip(a); 34 35 __gnu_cxx::hash_map<int, char> record; 36 37 void bfs() { 38 int temp, code; 39 std::queue<int> q; 40 q.push(origin); 41 while (!q.empty()) { 42 code = q.front(); 43 q.pop(); 44 temp = OP_A(code); 45 if (!record[temp]) 46 record[temp] = 'A', q.push(temp); 47 temp = OP_B(code); 48 if (!record[temp]) 49 record[temp] = 'B', q.push(temp); 50 temp = OP_C(code); 51 if (!record[temp]) 52 record[temp] = 'C', q.push(temp); 53 } 54 } 55 56 int main() { 57 bfs(); 58 int n, arr[8], i, j; 59 char s[30]; 60 while (scanf("%d", &n) && n != -1) { 61 for (i = 0; i < 8; i++) 62 scanf("%d", arr + i); 63 for (i = zip(arr), j = 0; i != origin && j < n; j++) { 64 s[j] = record[i]; 65 switch (s[j]) { 66 case 'A': 67 i = OP_A(i); 68 break; 69 case 'B': 70 i = resume_B(i); 71 break; 72 case 'C': 73 i = resume_C(i); 74 break; 75 } 76 } 77 if (i != origin) 78 printf("-1\n"); 79 else { 80 printf("%d ", j); 81 while (j--) 82 putchar(s[j]); 83 putchar('\n'); 84 } 85 } 86 return 0; 87 }
时间0.01s,空间1120kb。
至于最优的0.00s,本人能力有限实在想不出来了,若有大神赐教定将顶礼膜拜。