垃圾ACMer的暑假训练220721
4. 枚举、模拟与排序
4.1 连号区间数
题意
对某个排列,若区间 [ l , r ] [l,r] [l,r]内的所有元素升序排列后能得到一个长度为 ( r − l + 1 ) (r-l+1) (r−l+1)的连续序列,则称该区间为连号区间.
第一行输入一个整数 n ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e}4) n (1≤n≤1e4).第二行输入 1 ∼ n 1\sim n 1∼n的一个排列.对该排列,输出其中连号区间的个数.
思路
设区间 [ a , b ] [a,b] [a,b]中的最大、最小数分别为 m a x n u m maxnum maxnum、 m i n n u m minnum minnum,则 [ a , b ] [a,b] [a,b]是连号区间的充要条件是: m a x n u m − m i n n u m = b − a maxnum-minnum=b-a maxnum−minnum=b−a.
[证] (必) 显.
(充) 因序列是 1 ∼ n 1\sim n 1∼n的一个排列,则区间 [ a , b ] [a,b] [a,b]中的 ( b − a + 1 ) (b-a+1) (b−a+1)个数两两相异.若 m a x n u m − m i n n u m = b − a maxnum-minnum=b-a maxnum−minnum=b−a,则 [ a , b ] [a,b] [a,b]内的数是 m i n n u m ∼ m a x n u m minnum\sim maxnum minnum∼maxnum的一个排列.
时间复杂度 O ( n 2 ) O(n^2) O(n2),但常数很小,可过.
代码
const int MAXN = 1e5 + 5;
int n;
int a[MAXN];
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
int ans = 0;
for (int i = 0; i < n; i++) { // 区间左端点
int minnum = INF, maxnum = -INF;
for (int j = i; j < n; j++) {
minnum = min(minnum, a[j]), maxnum = max(maxnum, a[j]); // [i,j]中的最大、最小值
if (maxnum - minnum == j - i) ans++;
}
}
cout << ans;
}
4.2 递增三元组
题意
给定三个整数数组 a = [ a 1 , a 2 , ⋯ , a n ] , b = [ b 1 , b 2 , ⋯ , b n ] , c = [ c 1 , c 2 , ⋯ , c n ] a=[a_1,a_2,\cdots,a_n],b=[b_1,b_2,\cdots,b_n],c=[c_1,c_2,\cdots,c_n] a=[a1,a2,⋯,an],b=[b1,b2,⋯,bn],c=[c1,c2,⋯,cn].统计有多少个三元组 ( i , j , k ) s . t . a i < b j < c k ( 1 ≤ i , j , k ≤ n ) (i,j,k)\ s.t.\ a_i<b_j<c_k\ \ (1\leq i,j,k\leq n) (i,j,k) s.t. ai<bj<ck (1≤i,j,k≤n).
第一行输入一个整数 n ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n (1≤n≤1e5),表示数组长度.接下来三行每行输入 n n n个数,分别代表三个数组,数组元素范围 [ 0 , 1 e 5 ] [0,1\mathrm{e}5] [0,1e5].
思路
由数据范围知:至多枚举一个数组,显然应枚举 b b b,对每个 b [ i ] b[i] b[i],统计有多少个 a [ i ] a[i] a[i]和 c [ i ] c[i] c[i]满足要求,它们的个数之积即 b [ i ] b[i] b[i]的贡献.
对每个 b [ i ] b[i] b[i],统计 a a a中有几个 a [ j ] s . t . a [ j ] < b [ i ] a[j]\ s.t.\ a[j]<b[i] a[j] s.t. a[j]<b[i]的做法:①求 [ 1 , B [ i ] − 1 ] [1,B[i]-1] [1,B[i]−1]的前缀和;②排序 a a a,二分;③双指针;④BIT.
代码以①为例.因数组元素范围 [ 0 , 1 e 5 ] [0,1\mathrm{e}5] [0,1e5],可给每个元素 + 1 +1 +1,方便写前缀和数组.
代码
const int MAXN = 1e5 + 5;
int n;
int a[MAXN], b[MAXN], c[MAXN];
int cnta[MAXN], cntc[MAXN]; // cnta[i]表示a中有多少个数小于b[i],cntc[i]表示c中有多少个数大于b[i]
int cnt[MAXN], pre[MAXN]; // cnt[i]表示数i出现的次数,pre[]是cnt[]的前缀和
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i], a[i]++;
for (int i = 0; i < n; i++) cin >> b[i], b[i]++;
for (int i = 0; i < n; i++) cin >> c[i], c[i]++;
// 求cnta[]
for (int i = 0; i < n; i++) cnt[a[i]]++;
for (int i = 1; i < MAXN; i++) pre[i] = pre[i - 1] + cnt[i];
for (int i = 0; i < n; i++) cnta[i] = pre[b[i] - 1];
// 求cntc[]
memset(cnt, 0, so(cnt)), memset(pre, 0, so(pre));
for (int i = 0; i < n; i++) cnt[c[i]]++;
for (int i = 1; i < MAXN; i++) pre[i] = pre[i - 1] + cnt[i];
for (int i = 0; i < n; i++) cntc[i] = pre[MAXN - 1] - pre[b[i]];
ll ans = 0;
for (int i = 0; i < n; i++) ans += (ll)cnta[i] * cntc[i];
cout << ans;
}
4.3 特别数的和
题意
称数码中含 2 2 2、 0 0 0、 1 1 1、 9 9 9的数(不含前导零)为特别数.给定整数 n ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e4}) n (1≤n≤1e4),求 1 ∼ n 1\sim n 1∼n中特别数之和.
代码
set<int> s = { 2,0,1,9 };
int main() {
int n; cin >> n;
ll ans = 0;
for (int i = 1; i <= n; i++) {
int x = i;
while (x) {
int t = x % 10;
x /= 10;
if (s.count(t)) {
ans += i;
break;
}
}
}
cout << ans;
}
4.4 错误票据
题意
有某种票据,每张票据有唯一的ID号(不超过 1 e 5 1\mathrm{e}5 1e5的正整数),所有票据的ID号连续,但ID号的起始数码随机.因疏忽,录入ID号时发生了一处错误,造成某个ID断号,另一ID重号.求断号、重号的ID,假设断号不发生在最大、最小号.
第一行输入一个整数 n ( 1 ≤ n ≤ 100 ) n\ \ (1\leq n\leq 100) n (1≤n≤100),表示有 n n n行数据.接下来 n n n行每行输入若干个(不超过 100 100 100个)正整数,代表一个ID号.
第一行输出两个正整数,分别表示断号ID和重号ID.
代码
const int MAXN = 1e5 + 5;
int a[MAXN];
int main() {
int x; cin >> x;
int idx = 0;
while (cin >> x) a[idx++] = x;
sort(a, a + idx);
int ans1, ans2;
for (int i = 1; i < idx; i++) {
if (a[i] >= a[i - 1] + 2) ans1 = a[i] - 1; // 注意-1
else if (a[i] == a[i - 1]) ans2 = a[i];
}
cout << ans1 << ' ' << ans2;
}
3.1 数学与简单DP
3.1 买不到的数目
题意
糖有 n n n颗/包和 m ( 2 ≤ n , m ≤ 1000 ) m\ \ (2\leq n,m\leq 1000) m (2≤n,m≤1000)颗/包两种包装,包装不可拆开.顾客买糖时,通过两种包装的组合来凑出一定的数目,有些数目凑不出.求最大的不能凑出的数目,数据保证有解.
思路I
设 d = gcd ( n , m ) d=\gcd(n,m) d=gcd(n,m).若 d ≠ 1 d\neq 1 d=1,则 x n + y m xn+ym xn+ym是 d d d的倍数,进而不能凑出所有非 d d d的倍数的数.而数据保证有解,故 gcd ( n , m ) = 1 \gcd(n,m)=1 gcd(n,m)=1.
若 gcd ( n , m ) = 1 \gcd(n,m)=1 gcd(n,m)=1,由Bezout定理: ∃ a , b ∈ Z s . t . a n + b m = 1 \exists a,b\in\mathbb{Z}\ s.t.\ an+bm=1 ∃a,b∈Z s.t. an+bm=1.若要凑出数目 x x x,两边同乘 x x x得: a x n + b x m = x axn+bxm=x axn+bxm=x.
注意到 a x n − m n + b x m + m n = x ⇔ ( a x − m ) n + ( b x + n ) m = x axn-mn+bxm+mn=x\Leftrightarrow (ax-m)n+(bx+n)m=x axn−mn+bxm+mn=x⇔(ax−m)n+(bx+n)m=x,
x x x充分大时, a x − m , b x + n > 0 ax-m,bx+n>0 ax−m,bx+n>0,这表明: gcd ( n , m ) = 1 \gcd(n,m)=1 gcd(n,m)=1时必有解.
对互素的正整数 p , q p,q p,q,不能由 p x + q y ( x , y ≥ 0 ) px+qy\ \ (x,y\geq 0) px+qy (x,y≥0)表出的最大数为 p q − p − q pq-p-q pq−p−q.
[证] (1)先证 p q − p − q pq-p-q pq−p−q不能由 p x + q y px+qy px+qy表出.若不然,则 p x + q y = p q − p − q ⇔ p ( x + 1 ) + q ( y + 1 ) = p q px+qy=pq-p-q\Leftrightarrow p(x+1)+q(y+1)=pq px+qy=pq−p−q⇔p(x+1)+q(y+1)=pq.
这表明: q ∣ ( x + 1 ) q\mid (x+1) q∣(x+1),则 x + 1 ≥ q x+1\geq q x+1≥q.若 x + 1 = q x+1=q x+1=q,则 y = − 1 y=-1 y=−1,矛盾.故 x + 1 > q x+1>q x+1>q.
此时 p ( x + 1 ) > p q p(x+1)>pq p(x+1)>pq,则 q ( y + 1 ) < 0 q(y+1)<0 q(y+1)<0,矛盾.故 p q − p − q pq-p-q pq−p−q不能由 p x + q y px+qy px+qy表出
(2)再证 > p q − p − q >pq-p-q >pq−p−q的数都能由 p x + q y ( x , y ≥ 0 ) px+qy\ \ (x,y\geq 0) px+qy (x,y≥0)表出.
注意到 ( p − 1 ) ( q − 1 ) = p q − p − q + 1 (p-1)(q-1)=pq-p-q+1 (p−1)(q−1)=pq−p−q+1,则 n > p q − p − q n>pq-p-q n>pq−p−q即 n ≥ ( p − 1 ) ( q − 1 ) n\geq (p-1)(q-1) n≥(p−1)(q−1).
因 gcd ( p , q ) = 1 \gcd(p,q)=1 gcd(p,q)=1,则对 ∀ m < min { p , q } , ∃ a , b ∈ Z s . t . a p + b q = m \forall m<\min\{p,q\},\exists a,b\in\mathbb{Z}\ s.t.\ ap+bq=m ∀m<min{p,q},∃a,b∈Z s.t. ap+bq=m.不妨设 a > 0 > b a>0>b a>0>b.
若 a > q a>q a>q,取 a 1 = a − q , b 1 = b + q a_1=a-q,b_1=b+q a1=a−q,b1=b+q,则 a 1 p + b 1 q = m a_1p+b_1q=m a1p+b1q=m,若 a 1 > q a_1>q a1>q,重复上述操作.
此时 A p + B q = m Ap+Bq=m Ap+Bq=m,且 0 < ∣ A ∣ < q , 0 < ∣ B ∣ < p 0<|A|<q,0<|B|<p 0<∣A∣<q,0<∣B∣<p.
因 p q − p − q = ( q − 1 ) p − p pq-p-q=(q-1)p-p pq−p−q=(q−1)p−p,
对 ∀ n > p q − p − q \forall n>pq-p-q ∀n>pq−p−q,有 n = p q − p − q + k ⋅ min { p , q } + r n=pq-p-q+k\cdot \min\{p,q\}+r n=pq−p−q+k⋅min{p,q}+r,其中 r < m < min { p , q } r<m<\min\{p,q\} r<m<min{p,q}.
令 A p + B q = r Ap+Bq=r Ap+Bq=r,不妨设 A > 0 A>0 A>0,
则 n = ( q − 1 ) p − p + k ⋅ min { p , q } + A p + B q = ( A − 1 ) p + ( B + p ) q + k ′ ⋅ min { p , q } n=(q-1)p-p+k\cdot \min\{p,q\}+Ap+Bq=(A-1)p+(B+p)q+k'\cdot \min\{p,q\} n=(q−1)p−p+k⋅min{p,q}+Ap+Bq=(A−1)p+(B+p)q+k′⋅min{p,q}.
其中 A − 1 , B + q − 1 , k ′ ≥ 0 A-1,B+q-1,k'\geq 0 A−1,B+q−1,k′≥0.
代码I
int main() {
int n, m; cin >> n >> m;
cout << (n - 1) * (m - 1) - 1;
}
思路II
暴搜找规律:
bool dfs(int x, int m, int n) {
if (!x) return true;
if (x >= m && dfs(x - m, m, n)) return true;
if (x >= n && dfs(x - n, m, n)) return true;
return false;
}
int main() {
int n, m; cin >> n >> m;
int ans = 0;
for (int i = 1; i <= 1000; i++)
if (!dfs(i, n, m)) ans = i;
cout << ans;
}
3.2 蚂蚁感冒
题意
长度位 100 100 100的杆上有 n n n只蚂蚁,初始时有的头朝左,有的头朝右.每只蚂蚁沿杆以 1 1 1单位/秒的速度向前爬.两蚂蚁碰面时,它们同时掉头往相反的方向爬行.这些蚂蚁中有一只蚂蚁感冒了,它在与其他蚂蚁碰面时会将感冒传染给其他蚂蚁.当所有蚂蚁爬离杆时,求患感冒的蚂蚁的个数.
第一行输入 n ( 1 < n < 50 ) n\ \ (1<n<50) n (1<n<50),表示蚂蚁总数,蚂蚁编号 1 ∼ n 1\sim n 1∼n.第二行输入 n n n个整数 x i ( 0 < ∣ x i ∣ < 100 ) x_i\ \ (0<|x_i|<100) xi (0<∣xi∣<100), x i x_i xi的绝对值表示第 i i i只蚂蚁离杆左端点的距离, x i x_i xi为正表示头朝右,负值表示头朝左,地中 x 1 x_1 x1代表的蚂蚁感冒.数据保证初始时每只蚂蚁位置不同.
思路
注意到两蚂蚁碰面时同时掉头可看作两蚂蚁互相穿过对方继续前进,则经过一定时间内蚂蚁都会离开杆.
以初始时感冒的蚂蚁 x 1 x_1 x1(不妨设 x 1 > 0 x_1>0 x1>0)为分界线,其右边向左走的蚂蚁一定会被感染,右边向右走和左边向左走的蚂蚁一定不会被感染.对左边向右走的蚂蚁,若 x 1 x_1 x1右边存在向左走的蚂蚁,则左边向右走的蚂蚁一定会被感染,否则一定不会被感染.
代码
const int MAXN = 55;
int n;
int x[MAXN];
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> x[i];
int left = 0; // 左边向右走的蚂蚁数
int right = 0; // 右边向左走的蚂蚁数
for (int i = 1; i < n; i++) {
if (abs(x[i]) < abs(x[0]) && x[i] > 0) left++;
else if (abs(x[i]) > abs(x[0]) && x[i] < 0) right++;
}
if (x[0] > 0 && !right || x[0] < 0 && !left) cout << 1;
else cout << left + right + 1;
}
3.3 饮料换购
题意
对某饮料,集齐 3 3 3个瓶盖可换一瓶该饮料,该过程可一直循环.现你买入 n ( 0 < n < 1 e 4 ) n\ \ (0<n<1\mathrm{e}4) n (0<n<1e4)瓶该饮料,问在不浪费瓶盖的前提下最多能喝到多少瓶饮料.
思路
模拟.时间复杂度 O ( log 3 n ) O(\log_3 n) O(log3n).
代码
int main() {
int n; cin >> n;
int ans = n;
while (n >= 3) {
ans += n / 3;
n = n / 3 + n % 3;
}
cout << ans;
}
14.3 Flood Fill
Flood Fill算法用于在 O ( n ) O(n) O(n)的时间复杂度内找到某个点所在的连通块.
14.3.1 池塘计数
题意
有一片 n × m n\times m n×m的矩形土地,用字符矩阵表示,有水的单元格用’W’表示,不含水的单元格用’.'表示.每组相连的有水单元格视为一个池塘,每个单元格视为与其周围的 8 8 8个单元格相连.求池塘个数.
第一行输入整数 n , m ( 1 ≤ n , m ≤ 1000 ) n,m\ \ (1\leq n,m\leq 1000) n,m (1≤n,m≤1000).第二行起输入一个 n × m n\times m n×m的字符矩阵,表示土地的积水情况.
思路
一行一行扫描,若发现未被标记的水单元格,则以该单元格为起点做一次Flood Fill标记所有与其相连的单元格,更新答案.
代码
const int MAXN = 1005, MAXM = MAXN * MAXN;
int n, m;
char graph[MAXN][MAXN];
bool vis[MAXN][MAXN];
void bfs(int x, int y) {
qii que;
que.push({ x,y });
vis[x][y] = true;
while (que.size()) {
pii tmp = que.front(); que.pop();
for (int i = tmp.first - 1; i <= tmp.first + 1; i++) {
for (int j = tmp.second - 1; j <= tmp.second + 1; j++) {
if (i == tmp.first && j == tmp.second) continue; // 中间的格子
if (i < 0 || i >= n || j < 0 || j >= m) continue;
if (graph[i][j] == '.' || vis[i][j]) continue;
que.push({ i,j });
vis[i][j] = true;
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> graph[i];
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (graph[i][j] == 'W' && !vis[i][j]) {
bfs(i, j);
ans++;
}
}
}
cout << ans;
}
14.3.2 城堡问题
题意
1 2 3 4 5 6 7
#############################
1 # | # | # | | #
#####---#####---#---#####---#
2 # # | # # # # #
#---#####---#####---#####---#
3 # | | # # # # #
#---#########---#####---#---#
4 # # | | | | # #
#############################
# = Wall
| = No wall
- = No wall
方向:上北下南左西右东
上图是一个城堡的地形图,现要计算该城堡的房间数和最大房间的大小.
第一行输入整数 m , n ( 1 ≤ m , n ≤ 50 ) m,n\ \ (1\leq m,n\leq 50) m,n (1≤m,n≤50),分别表示城堡南北、东西方向的长度.接下来 m m m行每行输入 n n n格整数 p ( 0 ≤ p ≤ 15 ) p\ \ (0\leq p\leq 15) p (0≤p≤15),描述平面图对应位置的墙的特征,用 1 1 1、 2 2 2、 4 4 4、 8 8 8分别表示西墙、北墙、东墙、南墙, p p p为该方块包含的墙的数字之和.城堡的内墙被计算了两次,方块 ( 1 , 1 ) (1,1) (1,1)的南墙同时也是方块 ( 2 , 1 ) (2,1) (2,1)的北墙.数据保证城堡至少有两个房间.
输出共两行,第一行输出房间总数,第二行输出最大房间的面积(所含方格数).
思路
一个房间即一个连通块.
西、北、东、南分别视为一个方向 0 0 0、 1 1 1、 2 2 2、 3 3 3,判断每个方向有无墙只需判断该位置的 p p p值的对应二进制数位是否为 1 1 1.
代码
const int MAXN = 55, MAXM = MAXN * MAXN;
int n, m;
int graph[MAXN][MAXN];
bool vis[MAXN][MAXN];
int bfs(int x, int y) {
int area = 0;
qii que;
que.push({ x,y });
vis[x][y] = true;
while (que.size()) {
pii tmp = que.front(); que.pop();
area++;
for (int i = 0; i < 4; i++) { // 枚举四个方向
int curx = tmp.first + dx[i], cury = tmp.second + dy[i];
if (curx < 0 || curx >= n || cury < 0 || cury >= m) continue;
if (vis[curx][cury]) continue;
if (graph[tmp.first][tmp.second] >> i & 1) continue;
que.push({ curx,cury });
vis[curx][cury] = true;
}
}
return area;
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) cin >> graph[i][j];
int cnt = 0, maxarea = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!vis[i][j]) {
cnt++;
maxarea = max(maxarea, bfs(i, j));
}
}
}
cout << cnt << endl << maxarea;
}
14.3.3 山峰和山谷
题意
给定一个 n × n n\times n n×n的矩阵描述一个地图,其中格子 ( i , j ) (i,j) (i,j)的高度 h ( i , j ) h(i,j) h(i,j)给定.每个单元格视为与其周围的 8 8 8个单元格相邻.称一个格子的集合 S S S为山峰(山谷),如果:① S S S的所有格子都有相同的高度;② S S S的所有格子都连通;③对 s ∈ S s\in S s∈S,与 ∀ s \forall s ∀s相邻的 s ′ ∉ S s'\notin S s′∈/S,有 h s > h s ′ h_s>h_{s'} hs>hs′(山峰)或 h s < h s ′ h_s<h_{s'} hs<hs′(山谷);④若周围不存在相邻区域,则将其同时视为山峰和山谷.现要对给定的地图,求山峰和山谷的数量.若所有格子高度相同,则整个地图既是山峰也是山谷.
第一行输入整数 n ( 1 ≤ n ≤ 1000 ) n\ \ (1\leq n\leq 1000) n (1≤n≤1000),表示地图大小.第二行起输入一个 n × n n\times n n×n的矩阵,表示每个格子的高度 h ( 0 ≤ h ≤ 1 e 9 ) h\ \ (0\leq h\leq 1\mathrm{e}9) h (0≤h≤1e9).
输出山峰和山谷的数量.
思路
BFS时记录周围有无比当前格子高、低的格子,进而判断该连通块的类型.
代码
const int MAXN = 1005, MAXM = MAXN * MAXN;
int n, m;
int h[MAXN][MAXN];
bool vis[MAXN][MAXN];
void bfs(int x, int y, bool& has_higher, bool& has_lower) {
qii que;
que.push({ x,y });
vis[x][y] = true;
while (que.size()) {
pii tmp = que.front(); que.pop();
for (int i = tmp.first - 1; i <= tmp.first + 1; i++) {
for (int j = tmp.second - 1; j <= tmp.second + 1; j++) {
if (i == tmp.first && j == tmp.second) continue;
if (i < 0 || i >= n || j < 0 || j >= n) continue;
if (h[i][j] != h[tmp.first][tmp.second]) { // 山脉的边界
if (h[i][j] > h[tmp.first][tmp.second]) has_higher = true;
else has_lower = true;
}
else if (!vis[i][j]) {
que.push({ i,j });
vis[i][j] = true;
}
}
}
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) cin >> h[i][j];
int peak = 0, valley = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (!vis[i][j]) {
bool has_higher = false, has_lower = false;
bfs(i, j, has_higher, has_lower);
if (!has_higher) peak++;
if (!has_lower) valley++; // 注意不是else
}
}
}
cout << peak << ' ' << valley;
}
14.4 BFS最短路模型
14.4.1 迷宫
题意
给定一个 n × n ( 0 ≤ n ≤ 1000 ) n\times n\ \ (0\leq n\leq 1000) n×n (0≤n≤1000)的整型矩阵表示一个迷宫(下标从 0 0 0开始),其中 1 1 1表示墙壁, 0 0 0表示可走的路,玩家只能横竖走,不能斜着走.求从左上角到右下角的最短路径,数据保证有解.若有多组解,任输出一组解.
思路
开一个pii类型的 p r e [ ] [ ] pre[][] pre[][]数组记录每个格子的前驱.可从终点开始搜,这样从起点往回找的路径即正向路径.
代码
const int MAXN = 1005, MAXM = MAXN * MAXN;
int n, m;
int graph[MAXN][MAXN];
pii pre[MAXN][MAXN];
void bfs(int x, int y) {
memset(pre, -1, so(pre));
pre[x][y] = { 0,0 };
qii que;
que.push({ x,y });
while (que.size()) {
pii tmp = que.front(); que.pop();
for (int i = 0; i < 4; i++) {
int curx = tmp.first + dx[i], cury = tmp.second + dy[i];
if (curx < 0 || curx >= n || cury < 0 || cury >= n) continue;
if (graph[curx][cury]) continue;
if (~pre[curx][cury].first) continue;
que.push({ curx,cury });
pre[curx][cury] = tmp;
}
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) cin >> graph[i][j];
bfs(n - 1, n - 1); // 从终点开始搜
pii cur = { 0,0 };
while (true) {
cout << cur.first << ' ' << cur.second << endl;
if (cur.first == n - 1 && cur.second == n - 1) break;
cur = pre[cur.first][cur.second];
}
}
14.4.2 武士风度的牛
题意
用一个 n × m ( 1 ≤ n , m ≤ 150 ) n\times m\ \ (1\leq n,m\leq 150) n×m (1≤n,m≤150)的字符矩阵描述一个牧场,障碍用’*‘表示,草用’H’表示,其余位置用’.'表示.有一头牛,用字符’K’表示,可在牧场上跳"日"字,但它不能跳到树上或石头上.问该牛向吃到草至少要跳多少次.数据保证有解.
代码
const int MAXN = 155, MAXM = MAXN * MAXN;
int n, m;
char graph[MAXN][MAXN];
int dis[MAXN][MAXN];
int bfs() {
const int dx[8] = { -2,-1,1,2,2,1,-1,-2 }, dy[8] = { 1,2,2,1,-1,-2,-2,-1 };
pii cow;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (graph[i][j] == 'K') {
cow = { i,j };
break;
}
}
}
memset(dis, -1, so(dis));
dis[cow.first][cow.second] = 0;
qii que;
que.push(cow);
while (que.size()) {
pii tmp = que.front(); que.pop();
for (int i = 0; i < 8; i++) {
int curx = tmp.first + dx[i], cury = tmp.second + dy[i];
if (curx < 0 || curx >= n || cury < 0 || cury >= m) continue;
if (graph[curx][cury] == '*') continue;
if (~dis[curx][cury]) continue;
if (graph[curx][cury] == 'H') return dis[tmp.first][tmp.second] + 1;
que.push({ curx,cury });
dis[curx][cury] = dis[tmp.first][tmp.second] + 1;
}
}
}
int main() {
cin >> m >> n;
for (int i = 0; i < n; i++) cin >> graph[i];
cout << bfs();
}
14.4.3 抓住那头牛
题意
农夫和牛分别在数轴上的点 n n n、 k ( 0 ≤ n , k ≤ 1 e 5 ) k\ \ (0\leq n,k\leq 1\mathrm{e}5) k (0≤n,k≤1e5)处,农夫知道牛的位置,他想抓牛,假设牛不动.农夫每一步有两种移动方式:①从 x x x移动到 x − 1 x-1 x−1或 x + 1 x+1 x+1;②从 x x x移动到 2 x 2x 2x.问农夫至少需要多少步才能抓到牛.
思路
考虑最优解中至多用到数轴上坐标多少的点.显然最优解中不会用到负数坐标的点,因为走到负数坐标后还需走回正数.若 k < n k<n k<n,则不会使用 x → x + 1 x\rightarrow x+1 x→x+1或 x → 2 x x\rightarrow 2x x→2x的移动方式.直观上坐标范围为 [ 0 , 2 e 5 ] [0,2\mathrm{e}5] [0,2e5],但事实上 [ 0 , 1 e 5 ] [0,1\mathrm{e}5] [0,1e5]足够.
代码
const int MAXN = 1e5 + 5;
int n, k;
int dis[MAXN];
int bfs() {
memset(dis, -1, so(dis));
dis[n] = 0;
qi que;
que.push(n);
while (que.size()) {
int tmp = que.front(); que.pop();
if (tmp == k) return dis[k];
if (tmp + 1 < MAXN && dis[tmp + 1] == -1) { // x走到x+1
dis[tmp + 1] = dis[tmp] + 1;
que.push(tmp + 1);
}
if (tmp - 1 >= 0 && dis[tmp - 1] == -1) { // x走到x-1
dis[tmp - 1] = dis[tmp] + 1;
que.push(tmp - 1);
}
if (tmp * 2 < MAXN && dis[tmp * 2] == -1) { // x走到2x
dis[tmp * 2] = dis[tmp] + 1;
que.push(tmp * 2);
}
}
}
int main() {
cin >> n >> k;
cout << bfs();
}
14.5 多源BFS
14.5.1 矩阵距离
题意
给定一个 n × m ( 1 ≤ n , m ≤ 1000 ) n\times m\ \ (1\leq n,m\leq1000) n×m (1≤n,m≤1000)的 01 01 01矩阵 A A A,定义 A [ i ] [ j ] A[i][j] A[i][j]与 A [ k ] [ l ] A[k][l] A[k][l]间的Manhattan距离 d i s ( A [ i ] [ j ] , A [ k ] [ l ] ) = ∣ i − k ∣ + ∣ j − l ∣ dis(A[i][j],A[k][l])=|i-k|+|j-l| dis(A[i][j],A[k][l])=∣i−k∣+∣j−l∣.输出一个 n × m n\times m n×m的整数矩阵 B B B,其中 B [ i ] [ j ] = min 1 ≤ x ≤ n , 1 ≤ y ≤ m , A [ x ] [ y ] = 1 d i s ( A [ i ] [ j ] , A [ x ] [ y ] ) \displaystyle B[i][j]=\min_{1\leq x\leq n,1\leq y\leq m,A[x][y]=1}dis(A[i][j],A[x][y]) B[i][j]=1≤x≤n,1≤y≤m,A[x][y]=1mindis(A[i][j],A[x][y]).
思路
即求每个元素到最近的 1 1 1的距离.
类比图论中有多个起点,求点到其最近的起点的最短路时,可建立与多个起点相连的虚拟源点,再求目标点到虚拟源点的最短路.在本题中,只需先将 B B B中对应的 A A A中所有 1 1 1的位置初始化为 0 0 0并入队即可.
代码
const int MAXN = 1005, MAXM = MAXN * MAXN;
int n, m;
char graph[MAXN][MAXN];
int dis[MAXN][MAXN];
void bfs() {
memset(dis, -1, so(dis));
qii que;
for (int i = 1; i <= n; i++) { // 将A中所有1的位置入队
for (int j = 1; j <= m; j++) {
if (graph[i][j] == '1') {
dis[i][j] = 0;
que.push({ i,j });
}
}
}
while (que.size()) {
pii tmp = que.front(); que.pop();
for (int i = 0; i < 4; i++) {
int curx = tmp.first + dx[i], cury = tmp.second + dy[i];
if (curx < 1 || curx > n || cury < 1 || cury > m) continue;
if (~dis[curx][cury]) continue;
dis[curx][cury] = dis[tmp.first][tmp.second] + 1;
que.push({ curx,cury });
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> graph[i] + 1;
bfs();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) cout << dis[i][j] << " \n"[j == m];
}