题目链接:https://vjudge.net/problem/FZU-1977
Accept: 597 Submit: 2199
Time Limit: 1000 mSec Memory Limit : 32768 KB
Problem Description
The pollution of the earth is so serious that people can not survive any more. Fortunately, people have found a new planet that maybe has life, and we call it "Pandora Planet".
Leonardo Da Vinci is the only astronaut on the earth. He will be sent to the Pandora Planet to gather some plant specimens and go back. The plant specimen is important to the people to decide whether the planet is fit to live or not.
Assuming that Da Vinci can only move in an N×M grid. The positions of the plant specimens he wants to collect are all marked by the satellite. His task is to find a path to collect all the plant specimens and return to the spaceship. There are some savage beasts in the planet. Da Vinci can not investigate the grid with the savage beast. These grids are also marked by the satellite. In order to save time Da Vinci could only visit each grid exactly once and also return to the start grid, that is, you can not visit a grid twice except the start grid. You should note that you can choose any grid as the start grid.
Now he wants to know the number of different paths he can collect all the plant specimens. We only care about the path and ignore where the start grid is, so the two paths in Figure 1 are considered as the same.
Figure 1
Input
The first line of the input contains an integer T (T≤100), indicating the number of cases. Each case begins with a line containing two integers N and M (1≤N, M≤12), the size of the planet is N×M. Each of the following N lines contains M characters Gij(1≤i≤N, 1≤j≤M), Gij denotes the status of the grid in row i and column j, where 'X' denotes the grid with savage beast, '*' denotes the safe grid that you can decide to go or not, 'O' denotes the plant specimen you should collect. We guarantee that there are at least three plant specimens in the map.
Output
For each test case, print a line containing the test case number (beginning with 1) and the number of different paths he can collect all the plant specimens. You can make sure that the answer will fit in a 64-bit signed integer.
Sample Input
2 2
OO
O*
4 4
***O
XO**
**O*
XX**
Sample Output
Case 2: 7
Source
The 35th ACM/ICPC Asia Regional Fuzhou Site —— Online Contest
题意:
给出一个n*m(n、m<=12)的棋盘,‘X’表示不可走格子, ‘*’表示可选格子, ‘O’表示必走格子,问有多少种回路,使得回路经过所有必走格刚好一次?
题解:
1.此题(URAL1519 Formula 1)的加强版,多了“可选格子”。
2.怎么处理这个“可选格子”呢?答:
1) 对于当前格子,如果他是可选格子,且没有左、上插头。由于他是“可选”的,那么我们就可以走出两个分支:一是作为必走格子新建连通分量,而是作为不可走格子,直接转移到下一个格子。
2) 由于可选的格子加入,就不能像以往记录最后一个必走格子来结束回路了,因为在必走格子之后,可能还有可选格子,而可选格子同样可以作为结束回路的格子。
3) 为了解决如何结束回路的问题,给出了两种解决方案,两种方案都需要添加一个标记isend来记录是否已经形成回路。
方案一:
对于可走的格子(可选或者必走),如果它有左、上插头,且这两个插头属于同一个分量,那么我们就把他们接上(不管当前格子是否为最后一个必走格),形成了一个回路。然后,我们再去判断是否还有其他连通分量,如果有的话,那么这种方案肯定不合法,应当舍弃;如果没有其他连通分量,那么我们就暂且当它是合法的,再接下来的过程中,如果发现还有必走点,那么这种方案也就被发现是不合法的,所以也应当抛弃。
代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <set>
using namespace std;
typedef long long LL;
const int INF = 2e9;
const LL LNF = 9e18;
const int MOD = 1e9+;
const int MAXN = 1e5;
const int HASH = 1e4; int n, m;
int maze[][]; struct
{
int size, head[HASH], next[MAXN];
LL state[MAXN], sum[MAXN]; void init()
{
size = ;
memset(head, -, sizeof(head));
} void insert(LL status, LL Sum)
{
int u = status%HASH;
for(int i = head[u]; i!=-; i = next[i])
{
if(state[i]==status)
{
sum[i] += Sum;
return;
}
}
state[size] = status; //头插法
sum[size] = Sum;
next[size] = head[u];
head[u] = size++;
} }Hash_map[]; struct
{
int code[]; //用于记录轮廓线上每个位置的插头状态
int isend; //标记是否已经结束,即形成回路
LL encode(int m) //编码:把轮廓线上的信息压缩到一个longlong类型中
{
LL status = isend;
int id[], cnt = ;
memset(id, -, sizeof(id));
id[] = ;
for(int i = m; i>=; i--) //从高位到低位。为每个连通块重新编号,采用最小表示法。
{
if(id[code[i]]==-) id[code[i]] = ++cnt;
code[i] = id[code[i]];
status <<= ; //编码
status += code[i];
}
return status;
} void decode(int m, LL status) //解码:将longlong类型中轮廓线上的信息解码到数组中
{
memset(code, , sizeof(code));
for(int i = ; i<=m; i++) //从低位到高位
{
code[i] = status&;
status >>= ;
}
isend = status&;
} void shift(int m) //左移:在每次转行的时候都需要执行。
{
for(int i = m-; i>=; i--)
code[i+] = code[i];
code[] = ;
} }Line; void transfer_blank(int i, int j, int cur)
{
for(int k = ; k<Hash_map[cur].size; k++) //枚举上一个格子所有合法的状态
{
LL status = Hash_map[cur].state[k]; //得到状态
LL Sum = Hash_map[cur].sum[k]; //得到数量
Line.decode(m, status); //对状态进行解码
int up = Line.code[j]; //得到上插头
int left = Line.code[j-]; //得到下插头 if(Line.isend)
{
if(maze[i][j]==) continue; //如果已经结束了,但在后面却出现了必走格,说明这种情况非法。
Line.code[j] = Line.code[j-] = ; //否则,这个格就是可选格,在结束后就视为不可走格,直接转移
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
continue;
} if(!up && !left) //没有上、左插头,新建分量
{
if(maze[i+][j] && maze[i][j+]) //如果新建的两个插头所指向的两个格子可行,新建的分量才合法
{
Line.code[j] = Line.code[j-] = ; //为新的分量编号,最大的状态才为6
Hash_map[cur^].insert(Line.encode(m), Sum);
}
if(maze[i][j]==) //如果为可选点,那么在没有插头的时候,可以选择不走
{
Line.code[j] = Line.code[j-] = ;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
}
else if( (left&&!up) || (!left&&up) ) //仅有其中一个插头,延续分量
{
int line = left?left:up; //记录是哪一个插头
if(maze[i+][j]) //往下延伸
{
Line.code[j-] = line;
Line.code[j] = ;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
if(maze[i][j+]) //往右延伸
{
Line.code[j-] = ;
Line.code[j] = line;
Hash_map[cur^].insert(Line.encode(m), Sum);
}
}
else //上、左插头都存在,尝试合并。
{
if(up!=left) //如果两个插头属于两个联通分量,那么就合并
{
Line.code[j] = Line.code[j-] = ;
for(int t = ; t<=m; t++) //随便选一个编号最为他们合并后分量的编号
if(Line.code[t]==up)
Line.code[t] = left;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
else //由于不确定哪个是结束格,所以就不再根据结束格来判断,而是暂且认为这种情况合法,到后面才排除非法的
{
Line.code[j] = Line.code[j-] = ;
if(j==m) Line.shift(m);
Line.isend = ;
for(int t = ; t<=m; t++) //合并后看看是否只有一个连通分量
if(Line.code[t])
Line.isend = ;
if(Line.isend)
Hash_map[cur^].insert(Line.encode(m), Sum);
}
}
}
} void transfer_block(int i, int j, int cur)
{
for(int k = ; k<Hash_map[cur].size; k++)
{
LL status = Hash_map[cur].state[k]; //得到状态
LL Sum = Hash_map[cur].sum[k]; //得到数量
Line.decode(m, status);
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
} int main()
{
int T, kase = ;
scanf("%d", &T);
while(T--)
{
char s[];
scanf("%d%d", &n, &m);
memset(maze, false, sizeof(maze));
for(int i = ; i<=n; i++)
{
scanf("%s", s+);
for(int j = ; j<=m; j++)
{
if(s[j]=='X') maze[i][j] = ;
else if(s[j]=='O') maze[i][j] = ;
else maze[i][j] = ;
}
} int cur = ;
Hash_map[cur].init();
Hash_map[cur].insert(, );
for(int i = ; i<=n; i++)
for(int j = ; j<=m; j++)
{
Hash_map[cur^].init();
if(!maze[i][j])
transfer_block(i, j ,cur);
else
transfer_blank(i, j, cur);
cur ^= ;
} LL last_status = ;
LL ans = Hash_map[cur].size?Hash_map[cur].sum[last_status]:;
printf("Case %d: %I64d\n", ++kase, ans);
}
}
方案二:
跟方案一类似,只不过把 “在形成回路后,如果发现了必走点,就舍弃” 这个判断提前了。即:在形成回路的时候,既要判断是否还有其他连通分量,也要判断后面是否还有必走格。
代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <set>
using namespace std;
typedef long long LL;
const int INF = 2e9;
const LL LNF = 9e18;
const int MOD = 1e9+;
const int MAXN = 1e5;
const int HASH = 1e4; int n, m, last_x, last_y;
int maze[][];
bool hav[][]; struct
{
int size, head[HASH], next[MAXN];
LL state[MAXN], sum[MAXN]; void init()
{
size = ;
memset(head, -, sizeof(head));
} void insert(LL status, LL Sum)
{
int u = status%HASH;
for(int i = head[u]; i!=-; i = next[i])
{
if(state[i]==status)
{
sum[i] += Sum;
return;
}
}
state[size] = status; //头插法
sum[size] = Sum;
next[size] = head[u];
head[u] = size++;
} }Hash_map[]; struct
{
int code[]; //用于记录轮廓线上每个位置的插头状态
int isend;
LL encode(int m) //编码:把轮廓线上的信息压缩到一个longlong类型中
{
LL status = isend;
int id[], cnt = ;
memset(id, -, sizeof(id));
id[] = ;
for(int i = m; i>=; i--) //从高位到低位。为每个连通块重新编号,采用最小表示法。
{
if(id[code[i]]==-) id[code[i]] = ++cnt;
code[i] = id[code[i]];
status <<= ; //编码
status += code[i];
}
return status;
} void decode(int m, LL status) //解码:将longlong类型中轮廓线上的信息解码到数组中
{
memset(code, , sizeof(code));
for(int i = ; i<=m; i++) //从低位到高位
{
code[i] = status&;
status >>= ;
}
isend = status&;
} void shift(int m) //左移:在每次转行的时候都需要执行。
{
for(int i = m-; i>=; i--)
code[i+] = code[i];
code[] = ;
} }Line; void transfer_blank(int i, int j, int cur)
{
for(int k = ; k<Hash_map[cur].size; k++) //枚举上一个格子所有合法的状态
{
LL status = Hash_map[cur].state[k]; //得到状态
LL Sum = Hash_map[cur].sum[k]; //得到数量
Line.decode(m, status); //对状态进行解码
int up = Line.code[j]; //得到上插头
int left = Line.code[j-]; //得到下插头 if(Line.isend) //如果已经结束了,那么说明这个格是可选格,在此视为不可走格,直接转移到下一个格
{
Line.code[j] = Line.code[j-] = ;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
continue;
} if(!up && !left) //没有上、左插头,新建分量
{
if(maze[i+][j] && maze[i][j+]) //如果新建的两个插头所指向的两个格子可行,新建的分量才合法
{
Line.code[j] = Line.code[j-] = ; //为新的分量编号,最大的状态才为6
Hash_map[cur^].insert(Line.encode(m), Sum);
}
if(maze[i][j]==)
{
Line.code[j] = Line.code[j-] = ;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
}
else if( (left&&!up) || (!left&&up) ) //仅有其中一个插头,延续分量
{
int line = left?left:up; //记录是哪一个插头
if(maze[i+][j]) //往下延伸
{
Line.code[j-] = line;
Line.code[j] = ;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
if(maze[i][j+]) //往右延伸
{
Line.code[j-] = ;
Line.code[j] = line;
Hash_map[cur^].insert(Line.encode(m), Sum);
}
}
else //上、左插头都存在,尝试合并。
{
if(up!=left) //如果两个插头属于两个联通分量,那么就合并
{
Line.code[j] = Line.code[j-] = ;
for(int t = ; t<=m; t++) //随便选一个编号最为他们合并后分量的编号
if(Line.code[t]==up)
Line.code[t] = left;
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
else if(!hav[i][j]) //后面没有必走格子
{
Line.code[j] = Line.code[j-] = ;
if(j==m) Line.shift(m);
Line.isend = ;
for(int t = ; t<=m; t++) //合并后看看是否只有一个连通分量
if(Line.code[t])
Line.isend = ;
if(Line.isend)
Hash_map[cur^].insert(Line.encode(m), Sum);
}
}
}
} void transfer_block(int i, int j, int cur)
{
for(int k = ; k<Hash_map[cur].size; k++)
{
LL status = Hash_map[cur].state[k]; //得到状态
LL Sum = Hash_map[cur].sum[k]; //得到数量
Line.decode(m, status);
if(j==m) Line.shift(m);
Hash_map[cur^].insert(Line.encode(m), Sum);
}
} int main()
{
int T, kase = ;
scanf("%d", &T);
while(T--)
{
char s[];
scanf("%d%d", &n, &m);
memset(maze, false, sizeof(maze));
for(int i = ; i<=n; i++)
{
scanf("%s", s+);
for(int j = ; j<=m; j++)
{
if(s[j]=='X') maze[i][j] = ;
else if(s[j]=='*') maze[i][j] = ;
else
{
maze[i][j] = ;
last_x = i;
last_y = j;
}
}
}
memset(hav, false, sizeof(hav));
for(int i = ; i<=n; i++)
for(int j = ; j<=m; j++)
if(i<last_x || (i==last_x&&j<last_y))
hav[i][j] = true; int cur = ;
Hash_map[cur].init();
Hash_map[cur].insert(, );
for(int i = ; i<=n; i++)
for(int j = ; j<=m; j++)
{
Hash_map[cur^].init();
if(!maze[i][j])
transfer_block(i, j ,cur);
else
transfer_blank(i, j, cur);
cur ^= ;
} LL last_status = ;
LL ans = Hash_map[cur].size?Hash_map[cur].sum[last_status]:;
printf("Case %d: %I64d\n", ++kase, ans);
}
}
错误分析:
一开始的时候,我是是这样写的:
if(!maze[i][j])
transfer_block(i, j ,cur);
else if(maze[i][j]==)
transfer_blank(i, j, cur);
else
{
transfer_block(i, j ,cur);
transfer_blank(i, j, cur);
}
即:如果当前格子是可选格子,那么就可以分两个分支进行转移。
但是统计数会偏多,因为:
1.在上一个格子转移的时候,因为把当前格子看作是可走格子,所以就可以有插头引向当前格子。然后到了转移这个格子的时候,如果把当前格子看做是不可走格子,那么它的前提是没有插头引过来。然后在上一个格子转移的时候,就既有插头的情况,也有没有的情况,所以数量肯定偏大。解决方法是:把当前格子当成不可走格子时,加个判断,判断是否含有插头。
2.在形成回路之后,所以的可选格子一律看成是不可走格子,所以只需一个分支去转移到下一个格子,两个分支肯定会出现重复,使得数量偏大。