搜索专题 1 解析
前置声明,本题单代码基本基于 C++98 。
作者尽量自查了两三遍,但篇幅太长并未曾有精力详细审查。如果发现可疑性错误,极有可能是作者放飞自我导致的笔误,请私信作者。
作者对关键结论基本做了证明。如有不理解请私信作者。
作者对题解中的代码尽量进行了重测。如有不能 ac 请私信作者。
以后可能有时间(可能没有)会写更亲民更严谨也更符合人类的重置版题解。
以后可能会把能用位运算实现的搜索都加一份位运算代码(可能不会)。
A POJ-1321
题意:
给一个 \(n \times n\) 的棋盘,有若干个位置可以摆放棋子,标记上 # 。询问摆放 \(k\) 个相同的非攻击型直车的方案数。
\(k \leq n \leq 8\) 。
解析:
因为一行只能摆放一个车,观测数据规模,我们尝试按行对棋盘进行 \(dfs\) 。
为了保证复杂度正确,分析 \(dfs\) 树。(\(dfs\) 的状态空间不占据实际空间,而用于分析 \(dfs\) 的复杂度。)
考虑每行只能放一辆车,最多有 \(N+1\) 种放法(放第 \(1 \sim N\) 行或不放)。最坏能考虑放 \(N\) 行(中间可能有一些行选择了不放)。
状态个数的一个上界是 \(9^{8} \leq 4 \times 10^{7}\) (可以分析更紧的上界,但是这个上界已经足够)。
对于每个状态,放棋子的时候并没有多余的打标机或者状态维护,时间都是 \(O(1)\) 的。那么时间复杂度和状态空间大小同级。
实现:
我们考虑状态 \(dfs(dep, cnt)\) ,当前正在处理第 \(dep\) 层,总共放了 \(cnt\) 个车。
一行要么放,\(dfs(dep + 1, cnt + 1)\) ,要么不放 \(dfs(dep + 1, cnt)\) 。并跳到下一行。
行可以放了就换行,列可以打上标记,\(col(i)\) 表示第 \(i\) 列是否放过。回溯的时候回收标记。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=10,MAXM=1000001;
int n,m;
char gr[MAXN][MAXN];
bool col[MAXN];
int dfs(int dep,int cnt){
if(cnt==m)return 1; // 返回答案的出口先于退出递归的出口
else if(dep==n+1)return 0;
int ans=0;
// 1. choose
L(i,1,n)if(!col[i]){
if(gr[dep][i]=='.')continue;
col[i]=true;
ans+=dfs(dep+1,cnt+1);
col[i]=false;
}
// 2. not choose
ans+=dfs(dep+1,cnt);
return ans;
}
int SOLVE(){
L(i,1,n)scanf("%s",gr[i]+1);
L(i,1,n)col[i]=false;
return dfs(1,0);
return 0;
}
signed main(){
for(scanf("%d%d",&n,&m);n!=-1||m!=-1;scanf("%d%d",&n,&m)){
printf("%d\n",SOLVE());
}
return 0;
}
B POJ-2251
题意:
输入一个三维空间,存在一些障碍,询问从 \(s\) 到达 \(t\) 的最短路径长度。
解析:
考虑 \(x, y, z\) 选 \(1\) 个赋值 \(-1\) 或 \(1\) ,另外 \(2\) 个为 \(0\) 。建立出大小为 \(\binom{3}{1} \times 2 = 6\) 的三维方向数组。
添加边界处理,从 \(s\) 点出发,每个点入队之前打上标记,从队列末尾的叶子生长,构造 \(bfs\) 树。当访问到 \(t\) 点,\(t\) 点的深度就是最短路径长度。
时间复杂度可以宽松地估计为三维空间的大小 \(O(LRC) \leq 30^{3} \leq 3 \times 10^{4}\) ,可以满足时间限制。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=32;
struct Node{
int x,y,z,v;
Node(){}
Node(int x_,int y_,int z_,int v_):x(x_),y(y_),z(z_),v(v_){}
};
int n,m,h,sx,sy,sz,ans;
char g[MAXN][MAXN][MAXN];
bool vis[MAXN][MAXN][MAXN];
bool out(int x,int y,int z){return x<1||n<x||y<1||m<y||z<1||h<z;}
int SOLVE(){
L(i,1,n)L(j,1,m){
scanf("%s", g[i][j]+1);
}
L(i,1,n)L(j,1,m)L(k,1,h)if(g[i][j][k]=='S')sx=i,sy=j,sz=k;
static int dx[]={0,1,0,-1,0,0},dy[]={1,0,-1,0,0,0},dz[]={0,0,0,0,1,-1};
vis[sx][sy][sz]=true;
queue<Node> qe;
qe.push(Node(sx,sy,sz,0));
ans=1<<30;
while(!qe.empty()){
Node cur=qe.front();
int x=cur.x,y=cur.y,z=cur.z,v=cur.v;
if(g[x][y][z]=='E'){
ans=v;
break;
}
qe.pop();
L(i,0,5){
int nx=x+dx[i],ny=y+dy[i],nz=z+dz[i];
if(out(nx,ny,nz))continue;
if(g[nx][ny][nz]=='#')continue;
if(vis[nx][ny][nz])continue;
vis[nx][ny][nz]=true;
qe.push(Node(nx,ny,nz,v+1));
}
}
L(i,1,n)L(j,1,m)L(k,1,h)vis[i][j][k]=false;
return ans;
}
signed main(){
for(scanf("%d%d%d",&n,&m,&h);n!=0||m!=0||h!=0;scanf("%d%d%d",&n,&m,&h)){
int ac=SOLVE();
if(ac==1<<30)puts("Trapped!");
else printf("Escaped in %d minute(s).\n",ac);
}
return 0;
}
C POJ-3278
题意:
给定 \(0 \leq N,K \leq 10^{5}\) ,询问从 \(N\) 到 \(K\) 的最短时间。
每个时间可以选择两种操作之一:
- 在 \(X\) 走到 \(X - 1\) 或 \(X + 1\) ,
- 在 \(X\) 走到 \(2X\) 。
解析:
显然问题有解,因为可以单步走。
若 \(K \leq N\) ,只有执行第一种操作的可能。否则两种操作都会选择性执行。
我们可以细分成三种操作:\(X-1,X+1,2X\) 。估计 \(bfs\) 树的节点为 \(O(10^{5})\) 级别,尝试进行 \(bfs\) ,在不严格保证算法时间的情况下能得到最短时间。
可以证明 \(bfs\) 树的节点个数不超过 \(10^{5}+1\) 个。
证明:
后退只有 \(X-1\) ,中间状态能走到的下界不会越过 \(0\) 。
前进存在 \(2X\) 。假设 \(K=10^{5}\) ,倍增跳到了 \(K=10^{5} + 2\) ,多花费 \(2\) 步能到 \(K\) 。也可以在倍增之前退一步,然后 \(2X\) 跳到 \(K\) ,只多花费了一步。
对前进的情况稍微归纳一下,可以分析出对于 \(K \leq 10^{5}\) ,倍增跳到了 \(10^{5} + 2 t \ s.t. \ 1 \leq t\) 都会更坏。所以中间状能走到的上界不会越过 \(10^{5}\) 。
于是 \(bfs\) 节点的状态空间的约束范围严格是 \(0 \sim 10^{5}\) 这 \(10^{5} + 1\) 个点。可以保证 \(bfs\) 时间复杂度。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=100002;
const int DWL=0,UWL=100001;
int N,K;
int vis[MAXN];
bool out(int x){return x<DWL||UWL<x;}
signed main(){
scanf("%d%d",&N,&K);
queue<pair<int,int> > qe;
qe.push({N,0});
vis[N]=true;
while(!qe.empty()){
int u=qe.front().first,v=qe.front().second;
// printf("cur %d %d\n",u, v);
if(u==K){
return 0*printf("%d\n",v);
}
qe.pop();
if(!out(u-1)&&!vis[u-1])qe.push({u-1,v+1}),vis[u-1]=true;
if(!out(u+1)&&!vis[u+1])qe.push({u+1,v+1}),vis[u+1]=true;
if(!out(u*2)&&!vis[u*2])qe.push({u*2,v+1}),vis[u*2]=true;
}
return 0;
}
D POJ-3279
图论分析+二进制枚举(搜索)汇点状态强制定向。
题意:
给一个 \(N \times M (1 \leq N,M \leq 15)\) 的网格图,\(1\) 代表黑色,\(0\) 代表白色。踩一个格子会翻转这个格子和它上下左右四个格子(如果存在)。
输出一个矩阵,表示每个格子最少需要踩多少下。要求踩的次数总和最低,如果还有次数总和相同,则输出字典序最小的矩阵。
解析:
前言
首先翻转问题,显然是每个格子要么翻要么不翻。因为 \(2\) 次翻转会回到原来的状态,等于抵消。
一般我们遇到的,一维情况如踩一个格子影响它的后缀;二维情况如踩一个格子,影响以他为左端点的 \(C \times C\) 方格。我们从产生影响的格子往被影响的格子连边,那么能得到一个有向无环图 \(DAG\) 。
众所周知有向无环图(\(DAG\))能从入度为 \(0\) 源点开始,一边删除被使用过的源点,一边在新源点上 \(dp\) 或贪心。这个理论保证了上述情况直接贪心的正确性。
但是这题,一个格子如果影响其他格子,会像上下左右都连边,显然得不到一个 \(DAG\) 。
方向很乱怎么办?枚举汇点状态,强制定向。
钦定某条边界上的点状态固定,那么从这条边开始可以逆推到前一层(每个前驱“是否需要翻转以使已经固定的后继成立”都是固定的)。对于非边界上的某一层,这一层的点只能影响到当前一层和已被固定的后一层。于是我们构造出了一个 \(DAG\) 。
这里我们可以选择枚举上下左右底边上的所有点作为汇点,不妨是上底边(因为从这里开始二进制枚举,最先找到的答案字典序最小,减少代码量。否则还需存储所有答案再判)。
那么时间复杂度就是 \(O(2^{M} N)\) 。
- 当然如果其他题里 \(M, N\) 的数量级不同,\(O(2^{M} N) \neq O(2^{N} M)\) 。即使遇到这种情况,换个底边枚举依旧一样。
实际上图分析基本用于分析算法的时间复杂度和正确性,具体实现中并不会用到图这种数据结构。
例子
二进制枚举第 \(1\) 行的状态(每个位置翻还是不翻),使得这个状态固定。
从第 \(2\) 行开始,每个节点必须明确翻还是不翻,使得第 \(1\) 行成立(在翻转操作状态固定的情况下,整行是白色)。这种明确性使得第 \(2\) 行固定。
从第 \(3\) 行开始,每个节点必须明确翻还是不翻,使得第 \(2\) 行成立。这种明确性使得第 \(3\) 行固定。
……
从第 \(n\) 行开始,每个节点必须明确翻还是不翻,使得第 \(n-1\) 行成立。这种明确性使得第 \(n\) 行固定。
最后 \(1 \sim n\) 的翻转状态都被固定了,而只有第 \(n\) 行不确定颜色,我们检查第 \(n\) 行的颜色情况。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=16;
const int dx[]={0,1,0,-1},dy[]={1,0,-1,0};
int N,M,mi;
int gr[MAXN][MAXN],op[MAXN][MAXN],cgr[MAXN][MAXN],acop[MAXN][MAXN];
bool out(int i,int j){return i<1||N<i||j<1||M<j;}
void work(int x,int y){
op[x][y]^=1;
cgr[x][y]^=1;
L(k,0,3){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
cgr[nx][ny]^=1;
}
}
signed main(){
scanf("%d%d",&N,&M);
L(i,1,N)L(j,1,M)scanf("%d",&gr[i][j]);
mi=1<<30;
L(mask,0,(1<<M)-1){
L(i,1,N)L(j,1,M)op[i][j]=0,cgr[i][j]=gr[i][j];
int copcnt=0;
L(i,0,M-1)if(mask>>i&1)work(1,i+1),copcnt++;
L(i,2,N)L(j,1,M)if(cgr[i-1][j]==1)work(i,j),copcnt++;
int cnt0=0;
L(j,1,M)cnt0+=cgr[N][j]==0;
if(cnt0==M&&copcnt<mi){
mi=copcnt;
L(i,1,N)L(j,1,M)acop[i][j]=op[i][j];
}
}
if(mi==1<<30)return 0*puts("IMPOSSIBLE");
else L(i,1,N)L(j,1,M)printf("%d%c",acop[i][j]," \n"[j==M]);
return 0;
}
E POJ-1426
这题在网上的绝大部分搜索题解均为无法保证复杂度的解,只是在恰好能通过这题。 遇到变种问题会超时。
题意:
给定一个 \(n(n \leq 200)\) ,构造一个只由 \(0,1\) 组成的 \(10\) 进制数 \(x\) ,满足 \(n \mid x\) 。要求 \(x\) 不能超过 \(100\) 位,且可以证明不会超过。
解析:
解的存在性?
尝试构造性证明,用到一个数竞里的常见技巧 \(R_{i} = (10^{i} - 1) / 9\) (\(i\) 个 \(1\) 的十进制数)。
由鸽笼原理,\(R_1 \sim R_{n + 1}\) 一定存在两个数 \(\bmod n\) 同余。不妨是 \(R_{1} \leq R_{i} < R_{j} \leq R_{n + 1}\) ,那么 \(n \mid (x = R_{j} - R_{i})\) 就是一个高位前缀是 \(1\) ,低位后缀是 \(0\) 的解。
是不是直接枚举 \(R_i\) 就秒了?显然不是。因为理论最坏能达到 \(200\) 位数,显然违反了题目要求(题目应该是在故意卡数学构造做法)。但我们至少证明了解的存在性。
下面考虑构造一个最小解(如果解最小,则最短,则满足题目要求的不超过 \(100\) 位)。
不被保证的解法
考虑从 \(1\) (保证没有前导 \(0\) )开始暴力搜索每一位填充 \(0\) 还是 \(1\) 并逐渐增加十进制多项式的长度,理论上能搜到解,但并不保证时间复杂度(是指数级 \(O(2^{n})\) ),但恰好这题可以通过。而在变种问题(比如洛谷 P2841)中使用不保证时间复杂度的暴力算法将会超时。
考虑从 \(1\) 开始构造 \(bfs\) 树(这题 \(dfs, bfs\) 均可,因为不需要回溯)。
- 如果过程中某个节点 \(v\) 满足被 \(n\) 整除,即 \(v \equiv 0 (\bmod n)\) ,则 \(v\) 就是解。
- 如果过程中某个节点 \(v\) 满足和之前构造出的节点 \(u\) 模 \(n\) 同余,即 \(v \equiv u (\bmod n)\) 。那么从 \(u\) 出发使得 \(u \equiv 0 (\bmod n)\) 的后缀和从 \(u\) 出发使得 \(v \equiv 0 (\bmod n)\) 的后缀是一样的,一样的后缀意味着 bfs 树上 \(u,v\) 具有一样的子树,我们可以直接剪掉重复子树。
- 剪掉重复子树的具体做法是,如果 \(v\) 和之前一个 \(u\) 模 \(n\) 同余,那么 \(u\) 就不继续往后生长。
从 \(1\) 出发构造 \(bfs\) 树(也是二叉树), 会往后生长的节点只有 \(O(n)\) 个(模 \(n\) 的模数最多 \(n\) 个),停止生长的节点有两个叶子(往后补一个 \(0\) 或 \(1\)),状态空间最大 \(O(n) ,\)bfs$ 树的节点最多有 \(O(n)\) 个。
涉及到存储高精度数和高精度对低精度取模的操作。每个节点可以使用一个 \(O(n)\) 长度的字符串存储高精度数,而高精度取模时间为 \(O(n)\) 。
高精度取模
高精度取模基于 \((x \times y + z) \bmod m = ((x \times y \bmod m) + y) \bmod m\) ,实际代码很简单:
N=0; for(int i=0;i<(int)SZ(str)-1;i++) N=(N*10+(str[i]-'0'))%MOD;
状态空间 \(O(n)\) ,状态空间中节点的转移时间 \(O(n)\) (每次转移需要进行一次高精度取模),总时间复杂度 \(O(n^{2})\) 。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
int operator%(const string &v,const int m){
int ans=0;
for (int i=0;i<SZ(v);i++)ans=(ans*10+v[i]-'0')%m;
return ans;
}
int PutStr(string str){
for (int i=0;i<SZ(str);i++)printf("%c",str[i]);
return 0;
}
int PutStrl(string str){
return PutStr(str)*puts("");
}
int N;
int vis[201];
int SOLVE(){
L(i,0,N-1)vis[i]=false;
queue<string> qe;
qe.push("1");
vis[1]=true;
while(!qe.empty()){
string x=qe.front();
qe.pop();
for(char ch='0';ch<='1';ch++){
string cur=x+ch;
int r=cur%N;
if(r==0) {
PutStrl(cur);
return 0;
}
else if(!vis[r])vis[r]=true,qe.push(cur);
}
}
return 0;
}
signed main(){
// PutStr("azfA1");
L(i,1,8)assert(string("8")%i==8%i);
for(scanf("%d",&N);N;scanf("%d",&N)){
int ac=SOLVE();
}
return 0;
}
F POJ-3126
题意:
\(100\) 次询问,每次给定两个四位素数 \(N, M\) 。要求\(N\) 每次只能改变一个数位变成另一个素数,询问 \(N\) 最少花多少步变成 \(M\) 。
解析:
注意到 \(4\) 位数的素数并不多。我们可以用我们爱用的办法,随便什么素数筛或者暴力预处理每个四位数是否是素数,都不会超时。
实际上,根据素数定理,设 \(\phi(n)\) 为不超过 \(n\) 的素数个数,有 \(\phi(n) \sim \frac{n}{\ln n}\) 。即 \(\leq 10^{5}\) 的素数大致满足 \(\frac{10^{5}}{11} \leq 10^{4}\) 。这 \(10^{4}\) 是状态空间的上限。
从 \(N\) 构造 \(bfs\) 树,树的节点不超过 \(10^{4}\) 。
考虑每个节点的转移,每次允许一个数位变化,每个数位能变成其他 \(9\) 个数(除了最高位)。那么每个节点的分支个数是 \(\binom{4}{1} \times 9 = 36 \leq 40\) 。状态个数乘以转移时间为 \(10^{4} \times 40 = 4 \times 10^{5}\) 。
对于一百次询问,总共用时 \(4 \times 10^{5} \times 100 = 4 \times 10^{7}\) ,不会超时。
实现:
只需要额外注意数位的枚举。
比如四位数 \(X=\overline{a_3 a_2 a_1 a_0}=a_3 \times 10^3 + a_2 \times 10^2 + a_1 \times 10^1 + a_0\) 。
枚举 \(i \in [0, 3]\) ,\(a_i = \lfloor \frac{X}{10^{i}} \rfloor \bmod 10\) 。然后让 \(v\) 替代 \(a_i\) ,即 \(\left ( \lfloor \frac{X}{10^{i}} \rfloor + (v - a_i) \right ) \times 10^{i} + X \bmod 10^{i}\) 。
由于不能有前导 \(0\) ,需要保证最高位为 \(1\) 。当 \(i=3\) 是最高位,\(v \in [1, 9]\) ,否则 \(v \in [0, 9]\) 。
理论上应该严格满足 \(v \neq d\) 。但是这里 \(v = d\) 已经被标记过了,稍微乱写一点也不会导致错误。
int p=1;
L(i,0,3){
int d=X/p%10;
L(j,i==3?1:0,9)if(d!=j){
int Y=(X/p+(j-d))*p+X%p;
}
p*=10;
}
线性筛筛出的 \(pmi(x) = x\) 表示 \(x\) 是素数,否则不是。当然可以用其他筛法,甚至直接 \(O(\frac{10^{5}}{\ln 10^{5}} \times \sqrt{10^{5}}) \leq 10^{8}\) 暴力预处理。
代码:
view coe
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=100000;
int pmi[MAXN],pr[MAXN/5],tot;
int vis[MAXN];
int N,M;
struct Node{
int x,step;
Node(){}
Node(int _x,int _step):x(_x),step(_step){}
};
int SOLVE(){
L(i,0,MAXN-1)vis[i]=false;
scanf("%d%d",&N,&M);
if(N>M)swap(N,M);
queue<Node> qe;
qe.push(Node(N,0));
vis[N]=true;
while(!qe.empty()){
int X=qe.front().x,step=qe.front().step;
if(X==M){
return step;
}
qe.pop();
int p=1;
L(i,0,3){
int d=X/p%10;
L(j,i==3?1:0,9)if(d!=j){
int Y=(X/p+(j-d))*p+X%p;
if(pmi[Y]!=Y)continue;
if(vis[Y])continue;
vis[Y]=true;
qe.push(Node(Y,step+1));
}
p*=10;
}
}
return -1;
}
signed main(){
tot=0;
pmi[1]=1;
L(i,2,MAXN-1){
if(!pmi[i])pmi[i]=i,pr[++tot]=i;
L(j,1,tot){
if(pr[j]*i>=MAXN)break;
pmi[pr[j]*i]=pr[j];
if(pmi[i]==pr[j])break;
}
}
assert(pr[5]==11);
for(int T,tc=1*scanf("%d",&T);tc<=T;tc++){
int ac=SOLVE();
if(ac==-1)puts("Impossible");
else printf("%d\n",ac);
}
return 0;
}
G POJ-3087
题意:
可以抽象成这样。
- 给 \(a_1, a_2, \cdots, a_n\) 和 \(b_1, b_2, \cdots, b_n\) 。
- 合并成 \(b_1, a_1, b_2, a_2, \cdots, b_n, a_n\) 。
- 询问 \(b_1, a_1, b_2, a_2, \cdots, b_n, a_n\) 是否和 \(c_1, c_2, \cdots, c_{2n}\) 在有序数值上相同。如果是则停止,否则执行 \(3\) 。
- 平分,前半部分重新标号成 \(a_1^{'}, a_2^{'}, \cdots, a_n^{'}\) ,后半部分重新标号 \(b_1^{'}, b_2^{'}, \cdots, b_n^{'}\) 。执行 \(2\) 。
问最快执行“拆开、合并”操作多少次,给定的序列能够和已知的序列颜色一样。
解析:
因为一开始给定的 \(a, b\) 已经是拆开的,为了一般话,我们不妨把他们拼接成 \(a_1, a_2, \cdots, a_n, b_1, b_2, \cdot, b_n\) 当作是第 \(0\) 个序列。
然后我们从第 \(1\) 个序列开始判断答案。
为了方便分析,我们把 \(a_1, a_2, \cdots, a_n, b_1, b_2, \cdot, b_n\) 到 \(b_1, a_1, b_2, a_2, \cdots, b_n, a_n\) ,看成 \(A_1, A_2, \cdots, A_n, A_{n + 1}, A_{n + 2}, \cdots, A_{2n}\) 到 \(A_{n+1}, A_{1}, A_{n + 2}, A_{2}, \cdots, A_{2n}, A_{n}\) 。
这显然是一个置换
一个 \(N\) 排列的置换 \(f\) 即 \(f(i) = p_i \quad s.t. \quad i = 1,2,\cdots,N\) 。
显然 \(f\) 看起来是将前 \(n\) 个元素顺序映射到偶数位置,后 \(n\) 个元素顺序映射到奇数位置。
实际上,这个 \(f\) 满足 \(f(i) = 2 i \bmod (2n + 1)\) 。
问题即给定 \(A,B\) 两个排列,每个数染上一个颜色。询问最小的 \(k\) ,使得 \(f^{k} \circ A\) 的颜色和 \(B\) 的颜色一样。
考虑最快让 \(A\) 和 \(B\) 的颜色相同。因为 \(A\) 的每次变换都是固定的,所以是在问第一次 \(A\) 和 \(B\) 的颜色相同是什么时候。
设 \(l\) 为最小的正整数满足 \(f^{l} \circ A = A\) ,我们称作 \(l\) 为 \(f\) 的阶。
- 换句话说就是最小的 \(l\) 满足 \(f^{l} (i) = i \quad s.t. \quad i=1,2,\cdots,N\) 。
首先。考虑某个 \(N\) 的置换 \(f^{'}\) ,每个点只有一个入度一个出度,会构成若干个环。\(f^{'}\) 的阶 \(l^{'}\) 即是所有环长的公倍数。
举个例子
比如
置换 \(f^{'}\) 可以分解为环 \(g_1,g_2\) 的直积,即 \(f^{'}\) 能分解出到两个环。
显然一个环是 \(1 \rightarrow 2 \rightarrow 3 \rightarrow 1\) ,另一个环是 \(4 \rightarrow 5 \rightarrow 4\) 。
\(1, 2, 3, 4, 5\) 通过 \(f_1\) 置换 \(lcm(3, 2) = 6\) 次才能复原。
一般来说,随机给定一个置换 \(f\) ,那么 \(l\) 最坏可能是指数级的。(比如最宽松的估计是 \(N!\) ,因为总共有 \(N!\) 种排列)
但考虑这里的置换 \(f(i) = 2 i \bmod (2n + 1)\) ,\(f^{2}(i) = f(f(i)) = 2 ( 2 \cdot i \bmod (2n + 1) ) \bmod (2n + 1) = 2^{2} \cdot i \bmod (2n + 1)\) ,归纳有 \(f^{x}(i) = 2^{x} \cdot i \bmod (2n + 1)\) 。
由于 \(gcd(2, 2n+1) = 1\) ,众所周知的欧拉定理保证了
众所周知最小循环是任意循环的约数,于是也有 \(l \leq \varphi(2n + 1) < 2n + 1\) 。
设 \(N = 2n\) ,则最坏 \(O(N)\) 次置换能让 \(A\) 循环,每次置换时间为 \(O(N)\) 。总时间复杂度 \(O(N^{2})\) 。
于是我们暴力地置换,查询 \(f^{x} \circ A\) 是否和 \(B\) 的颜色相同即可。直接套 \(set\) 查询会多带个 \(O(log N)\) ,手动字符串哈希并手写哈希表可以避免这个 \(log\) 。为了方便,不妨按使用 \(set\) 计算,单组测试的时间复杂度 \(O(N^{2} \log N)\) 。
测试数据组数 \(T \leq 1000\) 。实际时间不超过 \(1000 \times 200^{2} \times \ln 200 \leq 3 \times 10^{8}\) 。
看起来 \(3\) 的常数可能会 \(T\) ,实际上我们前面分析的 \(l\) 是 \(\varphi(2n+1)\) 的因子,于是我们就粗暴地按照 \(l=\varphi(2n+1)\) 进行了估算。所以常数会比估出来的小,实际上很难 \(TLE\) 。
- 真 \(TLE\) 了把 \(set\) 换成无序 \(set\) 或者手写哈希,常数就无了。
课外知识
实际上 \(l = \delta_{2n+1}(2)\),即 “\(f\) 的阶 \(l\)” 等于 “\(2\) 模 \(2n+1\) 的阶 \(\delta_{2n+1}(2)\) ” 。当且仅当 \(2\) 是 \(2n+1\) 的原根,才存在 \(\varphi(2n+1)=l=\delta_{2n+1}(2)\) ,否则 \(l=\delta_{2n+1}\) 一定严格是 \(\varphi(2n+1)\) 的真因子。这意味着上述估出时间复杂度不仅跑不满,而且大概率常数非常小。
实现:
主打一个爱怎么暴力怎么暴力。
可以直接建出 \(p_i\) 然后用 \(i \rightarrow p_i\) 做映射。也可以只是简单的奇偶交替插入空字符串以得到新字符串。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=202;
int N;
int p[MAXN];
char s1[MAXN],s2[MAXN],s3[MAXN];
set<string> S;
string str,pat;
char nxs[MAXN];
int SOLVE(){
S.clear();
scanf("%d%s%s%s",&N,s1+1,s2+1,s3+1);
str=pat="";
L(i,1,2*N){
if(i<=N)str+=s1[i];
else str+=s2[i-N];
pat+=s3[i];
}
L(i,1,2*N){
if(i<=N)p[i]=i*2;
else p[i]=(i-N)*2-1;
}
L(i,1,2*N){
L(j,1,2*N)nxs[p[j]]=str[j-1];
L(j,1,2*N)str[j-1]=nxs[j];
S.insert(str);
if(S.count(pat)){
return i;
}
}
return -1;
}
signed main(){
int T=1;
for(int tc=1*(scanf("%d",&T));tc<=T;tc++){
int ac=SOLVE();
printf("%d %d\n",tc,ac);
}
return 0;
}
H POJ-3414
题意:
有容量 \(A\) 和 \(B\) 升的两个水壶,允许以下操作:
- 将其中一个水壶注满水
- 将其中一个水壶的水倒掉
- 将其中一个水壶的水倒到另一个水壶,使得被倒的水壶装满水,或者倒水的水壶倒空。
查询一个最短的操作序列,使得其中一个水壶有恰好 \(C\) 升水。或者无法做到。
\(1 \leq A,B \leq 100, C \leq max(A,B)\) 。
解析:
考虑状态空间有多大,\(a\) 水壶的可能装 \(1 \sim A\) 的水,\(b\) 水壶可能装 \(1 \sim B\) 的水,精确的状态空间为 \(O(AB) \leq 10^{4}\) 。
状态空间不大,且要求最短路径,尝试 \(bfs\) 。
维护 \(x, y\) 分别是 \(a, b\) 水壶当前的水。考虑总共 \(6\) 种转移操作:
- \(a\) 倒满水:\(x = A\) 。
- \(a\) 倒掉水:\(x = 0\) 。
- \(a\) 倒水给 \(b\) :设 \(t = min(a, B - y)\) ,\(a-t,b+t\) 。
- \(b\) 倒满水:\(y = B\) 。
- \(b\) 倒掉水:\(y = 0\) 。
- \(b\) 倒水给 \(a\) :设 \(t = min(b, A - x)\) ,\(a+t,b-t\) 。
每次操作 \(O(1)\) ,状态空间上的每个节点要枚举满 \(6\) 个操作(而不是精确的能找到所有还没使用过的后继状态)。
那么直接从 \(x=0,y=0)\) 构造 \(bfs\) 树的时间复杂度就是 \(O(AB \times 6) = O(AB) \leq 10^{4}\) 的。
只是每次转移都要写六段代码,看起来代码会很,但是逻辑不复杂。
实现:
维护操作路径:我们知道 \(bfs\) 标记节点的过程是一棵树的生长过程,这棵树叫做 \(bfs\) 树。
给这棵树标号:维护一个时间戳 \(T\) ,每个节点被标记,就让他的 \(id\) 为 \(++T\) 。
获取路径:我们记每个节点的父亲,就能通过从目标节点开始一直跳父亲,跳到根节点。小技巧是我们使用栈存入逆的操作,那么取出来就是正的。
维护操作:对 \(6\) 种操作编号 \(1 \sim 6\) ,对每个节点记一个信息 \(x \in [1, 6]\) ,表示从它的父亲到它使用的是什么操作。
代码:
写六段操作,确实有点长,因为每个操作都要单独写。我不太确定有没有逻辑清晰且短小的代码。
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=11000;
struct Node{
int v1,v2,id,step;
Node(){}
Node(int _v1,int _v2,int _id,int _step):v1(_v1),v2(_v2),id(_id),step(_step){}
};
int vis[101][101];
int A,B,C,T,root;
int fa[MAXN];
int op[MAXN];
char s[6][10]={"FILL(1)","FILL(2)","DROP(1)","DROP(2)","POUR(1,2)","POUR(2,1)"};
signed main(){
scanf("%d%d%d",&A,&B,&C);
queue<Node> qe;
root=1;
fa[root]=-1;
T=root;
qe.push(Node(0,0,T,0));
vis[0][0]=true;
while(!qe.empty()){
int v1=qe.front().v1,v2=qe.front().v2,id=qe.front().id,step=qe.front().step;
qe.pop();
if(v1==C||v2==C){
printf("%d\n",step);
stack<int> stk;
for (int cur=id;cur!=root;cur=fa[cur]){
stk.push(cur);
}
while(!stk.empty()){
printf("%s\n",s[op[stk.top()]]);
stk.pop();
}
return 0;
}
// FILL(1)
if(!vis[A][v2]){
vis[A][v2]=true;
qe.push(Node(A,v2,++T,step+1));
fa[T]=id;
op[T]=0;
}
// FILL(2)
if(!vis[v1][B]){
vis[v1][B]=true;
qe.push(Node(v1,B,++T,step+1));
fa[T]=id;
op[T]=1;
}
// DROP(1)
if(!vis[0][v2]){
vis[0][v2]=true;
qe.push(Node(0,v2,++T,step+1));
fa[T]=id;
op[T]=2;
}
// DROP(2)
if(!vis[v1][0]){
vis[v1][0]=true;
qe.push(Node(v1,0,++T,step+1));
fa[T]=id;
op[T]=3;
}
// POUR(1,2)
int tv=min(B-v2,v1);
if(!vis[v1-tv][v2+tv]){
vis[v1-tv][v2+tv]=true;
qe.push(Node(v1-tv,v2+tv,++T,step+1));
fa[T]=id;
op[T]=4;
}
// POUR(2,1)
tv=min(A-v1,v2);
if(!vis[v1+tv][v2-tv]){
vis[v1+tv][v2-tv]=true;
qe.push(Node(v1+tv,v2-tv,++T,step+1));
fa[T]=id;
op[T]=5;
}
}
puts("impossible");
return 0;
}
I UVA-11624
题意:
给一个 \(R \times C\) 的二维矩阵,\(1 \leq R, C \leq 1000\) 。每个格子上存在障碍和道路,起点标号为 J ,有若干个点 F 标号为火。
每秒,火向四联通的邻居蔓延,你可以向四联通的邻居走一步。
如果一秒内,人和火都可以走到某个格子,认为火先走到。
询问不被烧到的情况下,最快走出地图的时间。或者不可能。
解析:
考虑 \(bfs\) ,状态空间 \(O(RC)\) ,转移时间 \(O(4 \times 1) = 1\) ,则 \(bfs\) 的时间为 \(O(RC)\) 。
所谓的多源 \(bfs\) ,本质上是单源 \(bfs\) 。
假设存在一个超级源点 \(root\) ,向 'J' 和所有 'F' 连一条边。让所有 'J' 先进入队列,'F' 最后进入。
\(bfs\) 树是,按照队列顺序一层一层生长的。那么对于每一层,总是火先行动,人再行动。
实现:
实际上我们并不加入超级源点 \(root\) ,只按顺序(先火后人)加入 \(root\) 的儿子。从根的下一层开始 \(bfs\) 即多源 \(bfs\) 。
如果人能走到边上花费 \(step\) 步,那么它下一步一定可以走出地图,火已经追不上了,答案则是 \(step + 1\) 。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=1002;
int N,M,ans;
char gr[MAXN][MAXN];
bool vis[MAXN-1][MAXN-1];
int dx[]={0,1,0,-1},dy[]={1,0,-1,0};
bool out(int x,int y){return x<1||N<x||y<1||M<y;}
struct Node{
int x,y,c,step;
};
int SOLVE(){
scanf("%d%d",&N,&M);
L(i,1,N)L(j,1,M)vis[i][j]=false;
L(i,1,N){
scanf("%s",gr[i]+1);
}
queue<Node> qe;
L(i,1,N)L(j,1,M)if(gr[i][j]=='F'){
qe.push({i,j,0,0});
vis[i][j]=true;
}
L(i,1,N)L(j,1,M)if(gr[i][j]=='J'){
qe.push({i,j,1,0});
vis[i][j]=true;
}
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y,c=qe.front().c,step=qe.front().step;
qe.pop();
if(x==1||x==N||y==1||y==M){
if(c==1){
return step+1;
}
}
L(k,0,3){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
if(gr[nx][ny]=='#')continue;
if(vis[nx][ny])continue;
vis[nx][ny]=true;
qe.push({nx,ny,c,step+1});
}
}
return -1;
}
signed main(){
int T=1;
for(int tc=1*(scanf("%d",&T));tc<=T;tc++){
int ac=SOLVE();
if(ac==-1)puts("IMPOSSIBLE");
else printf("%d\n",ac);
}
return 0;
}
J POJ-3984
题意:
给一个 \(5 \times 5\) 的矩阵,有通路、障碍、起点、终点,输出一条起点到终点的最短路径。
题解:
以起点为根构造 \(bfs\) 树,对 \(bfs\) 树生长过程中的节点标号,并记录每个节点的父亲和所在坐标。
走到终点开始往上跳父亲直到根节点,复原逆路径。然后按正路径向下走,输出节点编号对应的二维坐标。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=6;
int N,M;
int gr[MAXN][MAXN],T;
bool vis[MAXN][MAXN];
int loc[2][MAXN*MAXN],fa[MAXN*MAXN];
int dx[]={0,1,0,-1},dy[]={1,0,-1,0};
bool out(int x,int y){return x<1||N<x||y<1||M<y;}
struct Node{
int x,y,id;
};
signed main(){
N=M=5;
L(i,1,N)L(j,1,N)scanf("%d",&gr[i][j]);
queue<Node> qe;
qe.push({1,1,1});
vis[1][1]=true;
T=1;
fa[T]=-1;
loc[0][T]=1;
loc[1][T]=1;
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y,id=qe.front().id;
if(x==5&&y==5){
stack<int> stk;
for(int cur=id;cur!=-1;cur=fa[cur]){
stk.push(cur);
}
while(!stk.empty()){
printf("(%d, %d)\n",loc[0][stk.top()]-1,loc[1][stk.top()]-1);
stk.pop();
}
return 0;
}
qe.pop();
L(k,0,3){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
if(gr[nx][ny]==1)continue;
if(vis[nx][ny])continue;
vis[nx][ny]=true;
++T;
qe.push({nx,ny,T});
fa[T]=id;
loc[0][T]=nx;
loc[1][T]=ny;
}
}
return 0;
}
K HDU-1241
题意:
给一个 \(m \times n\) 的矩阵,'@' 代表油,'*' 代表空地。如果一个 '@' 的八连通邻居也是 '@' ,则这两块油属于同一块油田。询问总共有多少块油田。
解析:
上古时代这种解决这种问题的方法叫做“洪泛算法”,其实就是用 bfs 染色。其实这个算法以前考得挺难,这只是一道例题。所以我在后面又加了两道稍微进阶点的洪泛算法题。
那么我们暴力遍历整个矩阵,如果当前格子是 '@' ,就从 '@' 开始做八连通的染色,所到之处都染成一个非 '@' 的颜色。这里,甚至不妨是 '*' 。然后我们统计执行力多少次染色算法即可。
每个格子会被遍历到一次。每个 '@' 会被染成其他颜色一次。每个节点的转移时间是 \(O(4) = O(1)\) 。
单组测试,整个算法的时间复杂度不超过 \(O(n \times m + n \times m \times 4) = O(n \times m) \leq 10^{4}\) 。但题目没有说有多少组测试。
对于网格图:以前如果网格图很大,用 \(dfs\) 可能递归过深导致爆栈。(取决于评测机设置的栈区上限,有素质的出题人都会不卡栈区)这里并不大。
对于 \(XCPC\) 及以上算法竞赛风格的、更复杂多元的洪泛算法题:通常需要用 vis 数组染色,而不能简单的直接在原图上改标号染色染色。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=102;
int N,M,cnt;
char gr[MAXN][MAXN];
int dx[]={0,1,0,-1,1,1,-1,-1},dy[]={1,0,-1,0,1,-1,-1,1};
bool out(int x,int y){return x<1||N<x||y<1||M<y;}
struct Node{
int x,y;
};
void dfs(int x,int y){
gr[x][y]='*';
L(k,0,7){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
if(gr[nx][ny]=='*')continue;
gr[nx][ny]='@';
dfs(nx,ny);
}
}
int SOLVE(){
L(i,1,N)scanf("%s",gr[i]+1);
cnt=0;
L(i,1,N)L(j,1,M)if(gr[i][j]=='@'){
dfs(i,j);
cnt++;
}
return cnt;
}
signed main(){
for(scanf("%d%d",&N,&M);N!=0||M!=0;scanf("%d%d",&N,&M)){
printf("%d\n",SOLVE());
}
return 0;
}
L HDU-1495
题意:
给一瓶可乐 \(S\) ml ,给两个容量为 \(A\) ml 和 \(B\) ml 的杯子,满足 \(S=A+B\) 。这三个容器之间互相倒,询问最快平分可乐的次数,或者说做不到。
题目歧义的地方?
本来我以为平分是指,存在一个容器是 \(S/2\) ml 就行了,因为另外一个人直接拿走另外两个容器,全喝掉的话也喝到了 \(S/2\) ml。
研究了一下,这里的平分指的是,可乐里有 \(S/2\) ml ,容量更大的那个杯子有 \(S/2\) ml 。
解析:
如果 \(A < B\) ,不妨交换 \(A, B\) ,不影响结果。方便我们讨论第一个杯子总 \(\geq S/2\) 。
稍微做点简单的分析(即使不做这步分析,依旧不影响后面将要进行的对状态空间构建 bfs 树),因为只能倒出整数结果,如果 \(2 \nmid S\) ,显然不存在 \(\frac{S}{2}\) 。
三个容器的状态任意时刻都可以表示为 \(Ax+By\) 。所以 \(S / 2\) 一定至少是 \(gcd(A,B)\) 的倍数(Bezout 定理)。
证明:
三个容器的状态任意时刻都可以表示为 \(Ax+By\) 的证明:
初始的前几个状态:
- 初始时容器 \(s\)(可乐瓶)是 \(A \times 1 + B \times 1\) 。
- 先倒入容器 \(a\) (容量 \(A \geq S/2\) 的杯子),变成 \(A \times 0 + B \times 1\) 。
- 先倒入容器 \(b\) (容量 \(B \leq S/2\) 的杯子),变成 \(A \times 1 + B \times 0\) 。
- 容器 \(a\) 装满是 \(A \times 1 + B \times 0\) 。
- 容器 \(b\) 装满是 \(A \times 0 + B \times 1\) 。
考虑任意时刻,容器 \(a, b\) 已经表现成 \(A \times r_{A} + B \times s_{B}\) 和 \(A \times r_{B} + B \times s_{B}\) 。考虑容器 \(a\) 倒入容器 \(b\) (其他情况:任选两个容器,钦定一个倒入另一个,分析同理)。
- 若 $A \times (r_{A} + r_{A}) + B \times (s_{B} + s_{B}) \leq B $ ,即 $A \times (r_{A} + r_{A}) + B \times (s_{B} + s_{B} - 1) \leq 0 $ 。则容器 \(a\) 变成 \(A \times 0 + B \times 0\) ,容器 \(b\) 变成 \(A \times (r_{A} + r_{A}) + B \times (s_{B} + s_{B})\) 。
- 否则容器 \(a\) 变成 \(A \times (r_{A} + r_{A}) + B \times (s_{B} + s_{B} - 1)\) ,容器 \(b\) 变成 \(A \times 0 + B \times 1\) 。
\(\square\)
那么若 \(2 \nmid S\) 或者 \(gcd(A, B) \nmid S\) 则直接不需要往后考虑,已经是无解。
假设有解。注意到状态空间最大为 \((S+1)(A+1)(B+1)=O(SAB) \approx 10^{6}\) ,我们可以尝试使用遍历整个状态空间的算法。
考虑从状态 \((S,0,0)\) 构建 \(bfs\) 树,总共有 \(\binom{3}{2} \times 2! = 6\) 种操作,每种操作 \(O(1)\) ,则状态之间的转移时间为 \(O(6 \times 1) = O(1)\) 。然后直接 \(bfs\) 就好了。总的时间复杂度是 \(O(SAB)\) 。
如果寻遍了状态空间,都没有得到目标解,那么确实就是无解。因为我们一开始只分析了无解的两个显然的必要条件,并没有分析无解的充分条件。
- 实际上,我没办法证明我推导的必要条件已经构成了充分条件(即使他们好像确实是充分条件)。
- 另外,尽可能提前推导一些必要条件应该是分析问题的习惯。显然这题里,即使不推导这些条件,只要状态空间里搜索不到目标值,那就是无解。
和 POJ-3414 不同的是,前一题状态空间是二维,这一题状态空间是三维。由于要单独写六种操作,依旧会导致代码比较长,但是能保证逻辑清晰。倒水操作参考 POJ-3414 的解析。
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=101;
int S,N,M;
bool vis[MAXN][MAXN][MAXN];
struct Node{
int x,y,z,step;
};
int SOLVE(){
L(i,0,S)L(j,0,N)L(k,0,M)vis[i][j][k]=false;
queue<Node> qe;
qe.push({S,0,0,0});
vis[S][0][0]=true;
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y,z=qe.front().z,step=qe.front().step;
if(x==S/2&&y==S/2){
return step;
}
qe.pop();
int tv;
// A->B
tv=min(N-y,x);
if(!vis[x-tv][y+tv][z]){
vis[x-tv][y+tv][z]=true;
qe.push({x-tv,y+tv,z,step+1});
}
// A->C
tv=min(M-z,x);
if(!vis[x-tv][y][z+tv]){
vis[x-tv][y][z+tv]=true;
qe.push({x-tv,y,z+tv,step+1});
}
// B->A
tv=min(S-x,y);
if(!vis[x+tv][y-tv][z]){
vis[x+tv][y-tv][z]=true;
qe.push({x+tv,y-tv,z,step+1});
}
// B->C
tv=min(M-z,y);
if(!vis[x][y-tv][z+tv]){
vis[x][y-tv][z+tv]=true;
qe.push({x,y-tv,z+tv,step+1});
}
// C->A
tv=min(S-x,z);
if(!vis[x+tv][y][z-tv]){
vis[x+tv][y][z-tv]=true;
qe.push({x+tv,y,z-tv,step+1});
}
// C->B
tv=min(N-y,z);
if(!vis[x][y+tv][z-tv]){
vis[x][y+tv][z-tv]=true;
qe.push({x,y+tv,z-tv,step+1});
}
}
return -1;
}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
signed main(){
for(scanf("%d%d%d",&S,&N,&M);S!=0||N!=0||M!=0;scanf("%d%d%d",&S,&N,&M)){
if((S&1)||(S/2%gcd(N,M)!=0)){
puts("NO");
continue;
}
if (N<M)swap(N,M);
int ac=SOLVE();
if(ac==-1)puts("NO");
else printf("%d\n",ac);
}
return 0;
}
M HDU-2612
题意:
给一个图,有一些路 '.' 和障碍 '#' ,有两个起点 'Y' 和 'M' ,有若干个终点 '@' 。
询问是否存在一个点 '@' 满足 'Y' 和 'M' 到这个位置的最短距离之和最小。
解析:
不难反证法证明两个起点到某个终点的最短距离之和最小,则两个起点分别到这个终点走的是最短距离。否则,这两个起点到这个终点的最短距离之和可以更小。
实际上,从 'Y' 出发构造一次 \(bfs\) 树,处理出 \(f(i,j)\) 表示它到每个点的最短距离,从 'M' 出发构造一次 \(bfs\) 树,处理出 \(g(i,j)\) 表示它到每个点的最短距离。
遍历 \(1 \leq i \leq n, 1 \leq j \leq m\) ,\(f(i,j) + g(i,j)\) 的最小值就是答案。
注意按题意要求,答案最后要乘以 \(11\) 。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=202,MAXM=202;
struct Node{
int x,y,step;
};
int N,M,ans;
char gr[MAXN][MAXM];
bool vis[MAXN][MAXM];
int f[MAXN][MAXM],g[MAXN][MAXM];
int dx[]={0,1,0,-1},dy[]={1,0,-1,0};
bool out(int x,int y){return x<1||N<x||y<1||M<y;}
void work(int sx,int sy,int w[][MAXM]){
L(i,1,N)L(j,1,M)vis[i][j]=false;
queue<Node> qe;
qe.push({sx,sy,0});
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y,step=qe.front().step;
if(gr[x][y]=='@'){
w[x][y]=step;
}
qe.pop();
L(k,0,3){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
if(vis[nx][ny])continue;
if(gr[nx][ny]=='#')continue;
vis[nx][ny]=true;
qe.push({nx,ny,step+1});
}
}
}
int SOLVE(){
L(i,1,N)L(j,1,M)f[i][j]=g[i][j]=1<<30;
L(i,1,N)scanf("%s",gr[i]+1);
int sx1,sy1,sx2,sy2;
L(i,1,N)L(j,1,M){
if(gr[i][j]=='Y')sx1=i,sy1=j;
if(gr[i][j]=='M')sx2=i,sy2=j;
}
work(sx1,sy1,f);
work(sx2,sy2,g);
ans=1<<30;
L(i,1,N)L(j,1,M)if(f[i][j]!=1<<30&&g[i][j]!=1<<30){
ans=min(ans,f[i][j]+g[i][j]);
}
return ans*11;
return 0;
}
signed main(){
while(~scanf("%d%d",&N,&M)){
printf("%d\n",SOLVE());
}
return 0;
}
N POJ-1096
题意:
给一个 \(N \times M \times H\) 的三维立方体,立方体的每个坐标格子由一个由小正方体构成,编号为 \(0 \sim N \times M \times H - 1\) 。
输入若干个格子的编号,表示这些小正方体存在,否则就是不存在。
询问给出的三维模型的外表面积。
解析:
容易想到一个经典套路。从某个小正方形开始用洪泛算法染色,如果下一步会出边界,或者下一步不存在小正方形堵住,则停止搜索,同时总的外表面积加上 \(1 \times 1 \times 1\) 。
如果枚举每个存在的小正方形,如果能跑洪泛算法,就跑。那么至少会导致:
- 显然不仅外表面积会被计算,内表面积也会被计算。
- 如果是类似于封闭壳套封闭壳套封闭壳这种三维模型,每个封闭壳的表面积都会被计算。
这里我们用另一个经典套路,在整个三维模型外,围一圈超级封闭壳。
具体算法的一个实例描述是:
让超级封闭壳占据的格子染色为 \(2\) 。而原有的实际存在的小正方形染色为 \(1\) ,不存在小正方形的格子染色为 \(0\) 。我们从超级封闭壳的某个点开始洪泛算法,把 \(0\) 颜色的格子染成 \(2\) 的颜色,一旦下一步的格子颜色是 \(1\) ,则停止往这个方向染色,并让总的外表面积加上 \(1 \times 1 \times 1\) 。
这样我们就只计算了最外那层壳子的外表面积。(里面染不进去)。
注意我们如何把高维矩阵的编号和坐标之间做互相映射。一个经典的做法是变进制数函数映射(变进制数可以获取每个位数上的具体值。)
将坐标映射到变进制数的例子
以二维 \(N \times M\) 的矩阵为例:
对于 \(0 \leq i < N, 0 \leq j < M\) ,坐标 \((i, j)\) 映射到一阶变进制数 \(x = i \times M + j\) 。
\(j = x \bmod M\) ,\(i = \lfloor \frac{x}{M} \rfloor\) 。
以三维为例:
对于 \(0 \leq i < N, 0 \leq j < M, 0 \leq k < H\) ,坐标 \((i, j, k)\) 映射到一阶变进制数 \(x = i \times M \times H + j \times H + l\) 。
\(l = x \bmod H\) ,\(j = \lfloor \frac{x}{H} \rfloor \bmod M\) ,\(i = \lfloor \frac{x}{M \times H} \rfloor\) 。
更高维可以类推,但是实际上只会在比较难的数论或组合中考。
注意这种做法是从坐标 \((0, 0, 0)\) 开始映射的,而这个位置我们要那套超级封闭外壳(总不能用负数作数组索引),所以我们要把实际上算出来的坐标加上 \(1\) 的偏移量。
算法是显然的洪泛算法题。时间复杂度是显然的 \(O(N \times M \times H)\) 。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=62;
int N,M,H,n,ans;
int gr[MAXN][MAXN][MAXN];
int vis[MAXN][MAXN][MAXN];
struct P{
int x,y,z;
P(){}
P(int _x,int _y,int _z):x(_x),y(_y),z(_z){}
};
P hs(int v) {
return P(v/M/H,v/H%M,v%H);
}
bool out(int x,int y,int z){return x<0||N+1<x||y<0||M+1<y||z<0||H+1<z;}
int dx[]={0,1,0,-1,0,0},dy[]={1,0,-1,0,0,0},dz[]={0,0,0,0,1,-1};
int SOLVE(){
// i*M*H+j*M+k=id(i,j,k)
// i = id / M / H
// j = id / H % M
// k = id % H
L(i,0,N+1)L(j,0,M+1)L(k,0,H+1)gr[i][j][k]=0;
ans=0;
L(i,1,n){
int v;scanf("%d",&v);
P cur=hs(v);
gr[cur.x+1][cur.y+1][cur.z+1]=1;
}
gr[0][0][0]=2;
queue<P> qe;
qe.push(P(0,0,0));
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y,z=qe.front().z;
qe.pop();
L(k,0,5){
int nx=x+dx[k],ny=y+dy[k],nz=z+dz[k];
if(out(nx,ny,nz))continue;
if(gr[nx][ny][nz]==2)continue;
if(gr[nx][ny][nz]==1){
ans++;
}else if(gr[nx][ny][nz]==0){
gr[nx][ny][nz]=2;
qe.push(P(nx,ny,nz));
}else assert(false);
}
}
return ans;
}
signed main(){
for(scanf("%d%d%d%d",&N,&M,&H,&n);N!=0||M!=0||H!=0||n!=0;scanf("%d%d%d%d",&N,&M,&H,&n)){
int ac=SOLVE();
printf("The number of faces needing shielding is %d.\n",ac);
}
return 0;
}
O luogu-P9243
题意:
数据范围不说了,就是 \(O(n \times m)\) 能过的那种。
给一个 \(n \times m\) 的矩阵。上面有染了一种色的方块,如果一个四联通的色图,不被另一个四联通的色圈主,则这个色图就是以恶搞外壳。询问有多少个外壳。
解析:
不妨认为原图无色是 \(0\) ,有色是 \(1\) 。
显然同 POJ-1096 的思路,我们在最外层套一圈超级外壳,不妨染色成 \(2\) ,然后从超级外壳上的某个点(不妨是 \((0,0)\))开始做洪泛算法,把空无色格子都染成和超级外壳一种颜色。
于是可以圈住所有 \(1\) 色外壳。这时候再跑洪泛算法,把非颜色 \(2\) 的格子全部染成 \(3\) 颜色,计算执行了多少次算法,就有多少外壳。
注意点:
四联通也叫边联通,显然边联通不代表点联通。
所以需要圈住所有边联通的联通块,要用点联通的图去圈。也即我们从超级外壳上跑的洪泛算法是八连通而不是四联通的。
思考:如何圈住点联通的连通块?
边联通图可以圈住。
证明:
显然不能用点联通的图圈,因为会越到原有的点联通色块的内部。
容易证明只用边联通就染满圈点联通的外壳之外的所有格子。
反证法:任何一个再点联通外壳之外的格子,都存在一条到超级外壳的边联通路径,否则现在外壳之外还有外壳,那么现在的外壳实际上就是内壳,矛盾。
\(\square\)
实际上这种圈住外壳的问题,在游戏行业是常见的。
代码:
view code
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cassert>
#include<string>
#include<set>
#include<stack>
using namespace std;
typedef long long i64;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
#define SZ(a) int((a).size())
#define VI vector<int>
#define VL vector<i64>
const int MAXN=53;
char gr[MAXN][MAXN];
struct Node{
int x,y;
Node(){}
Node(int _x,int _y):x(_x),y(_y){}
};
int N,M,ans;
bool out(int x,int y){return x<0||N+1<x||y<0||M+1<y;}
int dx[]={0,1,0,-1,1,1,-1,-1},dy[]={1,0,-1,0,1,-1,1,-1};
void bfs_init(){
queue<Node> qe;
qe.push(Node(0,0));
gr[0][0]='2';
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y;
qe.pop();
L(k,0,7){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
if(gr[nx][ny]=='1')continue;
if(gr[nx][ny]=='2')continue;
gr[nx][ny]='2';
qe.push(Node(nx,ny));
}
}
}
void bfs_solve(int i,int j){
queue<Node> qe;
qe.push(Node(i,j));
gr[i][j]='3';
while(!qe.empty()){
int x=qe.front().x,y=qe.front().y;
qe.pop();
L(k,0,3){
int nx=x+dx[k],ny=y+dy[k];
if(out(nx,ny))continue;
if(gr[nx][ny]=='2')continue;
if(gr[nx][ny]=='3')continue;
gr[nx][ny]='3';
qe.push(Node(nx,ny));
}
}
}
int SOLVE(){
scanf("%d%d",&N,&M);
ans=0;
L(i,0,N+1)L(j,0,M+1)gr[i][j]='#';
L(i,1,N)scanf("%s",gr[i]+1);
bfs_init();
L(i,1,N)L(j,1,M)if(gr[i][j]!='2'&&gr[i][j]!='3'){
bfs_solve(i,j);
ans++;
}
return ans;
}
signed main(){
for(int T,tc=1*scanf("%d",&T);tc<=T;tc++){
printf("%d\n",SOLVE());
}
return 0;
}
写在最后
集训队倒闭了,所以这个题单也没了。
几乎是纯搜索。
刻意去除了所有搜索+其他算法的题。
刻意去除了所有实为 dp 记忆化实现形式的伪搜索。
浙公网安备 33010602011771号