垃圾ACMer的暑假训练220721

垃圾ACMer的暑假训练220721

4. 枚举、模拟与排序

4.1 连号区间数

题意

对某个排列,若区间 [ l , r ] [l,r] [l,r]内的所有元素升序排列后能得到一个长度为 ( r − l + 1 ) (r-l+1) (rl+1)的连续序列,则称该区间为连号区间.

第一行输入一个整数 n    ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e}4) n  (1n1e4).第二行输入 1 ∼ n 1\sim n 1n的一个排列.对该排列,输出其中连号区间的个数.

思路

设区间 [ 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 maxnumminnum=ba.

[] (必) 显.

(充) 因序列是 1 ∼ n 1\sim n 1n的一个排列,则区间 [ a , b ] [a,b] [a,b]中的 ( b − a + 1 ) (b-a+1) (ba+1)个数两两相异.若 m a x n u m − m i n n u m = b − a maxnum-minnum=b-a maxnumminnum=ba,则 [ a , b ] [a,b] [a,b]内的数是 m i n n u m ∼ m a x n u m minnum\sim maxnum minnummaxnum的一个排列.

时间复杂度 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  (1i,j,kn).

第一行输入一个整数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5),表示数组长度.接下来三行每行输入 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  (1n1e4),求 1 ∼ n 1\sim n 1n中特别数之和.

代码
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  (1n100),表示有 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  (2n,m1000)颗/包两种包装,包装不可拆开.顾客买糖时,通过两种包装的组合来凑出一定的数目,有些数目凑不出.求最大的不能凑出的数目,数据保证有解.

思路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,bZ 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 axnmn+bxm+mn=x(axm)n+(bx+n)m=x,

x x x充分大时, a x − m , b x + n > 0 ax-m,bx+n>0 axm,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,y0)表出的最大数为 p q − p − q pq-p-q pqpq.

[] (1)先证 p q − p − q pq-p-q pqpq不能由 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=pqpqp(x+1)+q(y+1)=pq.

​ 这表明: q ∣ ( x + 1 ) q\mid (x+1) q(x+1),则 x + 1 ≥ q x+1\geq q x+1q.若 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 pqpq不能由 p x + q y px+qy px+qy表出

(2)再证 > p q − p − q >pq-p-q >pqpq的数都能由 p x + q y    ( x , y ≥ 0 ) px+qy\ \ (x,y\geq 0) px+qy  (x,y0)表出.

​ 注意到 ( p − 1 ) ( q − 1 ) = p q − p − q + 1 (p-1)(q-1)=pq-p-q+1 (p1)(q1)=pqpq+1,则 n > p q − p − q n>pq-p-q n>pqpq n ≥ ( p − 1 ) ( q − 1 ) n\geq (p-1)(q-1) n(p1)(q1).

​ 因 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,bZ 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=aq,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 pqpq=(q1)pp,

​ 对 ∀ n > p q − p − q \forall n>pq-p-q n>pqpq,有 n = p q − p − q + k ⋅ min ⁡ { p , q } + r n=pq-p-q+k\cdot \min\{p,q\}+r n=pqpq+kmin{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=(q1)pp+kmin{p,q}+Ap+Bq=(A1)p+(B+p)q+kmin{p,q}.

​ 其中 A − 1 , B + q − 1 , k ′ ≥ 0 A-1,B+q-1,k'\geq 0 A1,B+q1,k0.

代码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 1n.第二行输入 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  (1n,m1000).第二行起输入一个 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  (1m,n50),分别表示城堡南北、东西方向的长度.接下来 m m m行每行输入 n n n格整数 p    ( 0 ≤ p ≤ 15 ) p\ \ (0\leq p\leq 15) p  (0p15),描述平面图对应位置的墙的特征,用 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 sS,与 ∀ 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  (1n1000),表示地图大小.第二行起输入一个 n × n n\times n n×n的矩阵,表示每个格子的高度 h    ( 0 ≤ h ≤ 1 e 9 ) h\ \ (0\leq h\leq 1\mathrm{e}9) h  (0h1e9).

输出山峰和山谷的数量.

思路

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  (0n1000)的整型矩阵表示一个迷宫(下标从 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  (1n,m150)的字符矩阵描述一个牧场,障碍用’*‘表示,草用’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  (0n,k1e5)处,农夫知道牛的位置,他想抓牛,假设牛不动.农夫每一步有两种移动方式:①从 x x x移动到 x − 1 x-1 x1 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 xx+1 x → 2 x x\rightarrow 2x x2x的移动方式.直观上坐标范围为 [ 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  (1n,m1000) 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])=ik+jl.输出一个 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]=1xn,1ym,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];
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值