9.18模拟赛总结

一.最大公约数

题面:
在这里插入图片描述

分析:
         考场上写 O ( n a i ) O(n\sqrt{a_i}) O(nai ) 做法艹过去了。
        我们考虑对每一个数枚举它的因子,看前面的范围中有没有数也有这个因子,然后更新答案。做完之后,范围要向后延伸一个位置,把那个位置的数的因子全部在桶里打上标记即可。肯定跑不满,复杂度可过。

二.子序列

题面:
在这里插入图片描述
在这里插入图片描述
分析:

        考场上只会60pts做法
         因为不能重复,我们考虑钦定 每次删除某一个连续相同字符段的最后一个。(因为删除这一连续段中任意一个都是一样的,会重复)并且某一个连续段如果删完了还要考虑左右段能不能合并。 我们将问题转化:题目上就是让求 有多少种删除位置的合法排列,我们规定 “合法” 就是 每次删一个位置时它都是某一个连续相同字符段的最后一个。
         考虑到如果 有两个合法删除位置排列的最后一个元素不同,那么这两个排列一定对应两种方案。因此我们可以想到 枚举最后删除的位置,那么左右两边是独立的,因此考虑 区间DP

         设 f [ l ] [ r ] f[l][r] f[l][r] 表示 [ l , r ] [l, r] [l,r] 的字符串删完并且最后删除的字符不等于 S r + 1 S_{r + 1} Sr+1(最后删除的字符不等于 S r + 1 S_{r + 1} Sr+1 的原因是 如果最后删除的字符等于 S r + 1 S_{r + 1} Sr+1,那么相当于最后删除的是一段连续相同字符段的第一个,而不是最后一个,和我们的钦定相悖,有可能重复)。转移就是 我们枚举最后一个删除的字符是哪一段的开头(最后删除的肯定是某一段的开头),设这个位置是 i i i,那么 左右贡献的方案数就是 f [ l ] [ i − 1 ] ∗ f [ i + 1 ] [ r ] f[l][i-1] * f[i+1][r] f[l][i1]f[i+1][r],同时因为左右是独立的,我们还要再乘上 C r − l i − l C_{r-l}^{i-l} Crlil,表示删除过程中一共有 r − l r-l rl个位置,选择 l − i l-i li个用来删左边,剩下的删右边。 因此 f [ l ] [ r ] = f [ l ] [ r ] + C r − l i − l ∗ f [ l ] [ i − 1 ] ∗ f [ i + 1 ] [ r ] f[l][r] = f[l][r] + C_{r-l}^{i-l} * f[l][i-1] * f[i + 1][r] f[l][r]=f[l][r]+Crlilf[l][i1]f[i+1][r]

CODE:

#include<bits/stdc++.h>//保证不重不漏 
using namespace std;
typedef long long LL;
const int N = 410;
int n;
LL f[N][N], C[N][N], mod = 1e9 + 7;
char c[N];
LL solve(int l, int r){
	if(l > r) return 1;//没了
	if(f[l][r] != -1) return f[l][r]; 
	LL ans = 0;
	for(int i = l; i <= r; i++){
		if(r == n || c[i] != c[r + 1]){// r == n就不用判断是否不等于c[r+1]了
			ans = (ans + ((solve(l, i - 1) * solve(i + 1, r)) % mod * C[r - l][i - l]) % mod) % mod;
		}
	}
	return f[l][r] = ans;
}
int main(){
	freopen("sub.in", "r", stdin);
	freopen("sub.out", "w", stdout);
	memset(f, -1, sizeof(f));
	scanf("%d", &n);
	for(int i = 0; i < N; i++){
		C[i][0] = 1LL;
		for(int j = 1; j <= i; j++){
			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
		}
	}
	scanf("%s", c + 1);
	printf("%lld\n", solve(1, n));
	return 0;
}


         q1:你在上面不是说 i i i 只能是某一相同字符段的开头吗?为什么代码里写的把这一段的所有位置都当做最后删考虑了?

         答:实际上只有当 i i i 是这一段的开头时才会有一个不为 0 0 0 的贡献,其他位置的贡献一定为 0 0 0。因为如果 i i i 不是某一段的开头,那么一定在向下递归的过程中都会有一个区间 [ l ′ , r ′ ] [l', r'] [l,r] 中有 s r ′ = s r ′ + 1 s_{r'} = s_{r' + 1} sr=sr+1 ,这里的 r ′ r' r 是递归的参数。最后一定有一段区间每一个字符都等于 s r ′ + 1 s_{r' + 1} sr+1,返回值就是 0 0 0。由于贡献是 相乘 的形式,因此最后贡献也就是 0 0 0

        也可以这样理解:在这种情况下,是一定删除不完的(因为我们要保证每次删除的都是某一段的末尾,而在 s r + 1 s_{r + 1} sr+1 删除之前, s r s_r sr 是不可能删除的)。因此如果对于区间 [ l , r ] [l, r] [l,r] s r = s r + 1 s_r = s_{r+1} sr=sr+1,最后的返回值一定是 0 0 0。 所以当 i i i不是某一段的开头时, f [ l ] [ i − 1 ] f[l][i-1] f[l][i1] 的返回值也一定为 0 0 0


         q2:怎样保证不重不漏?

        答:观察式子 f [ l ] [ r ] = f [ l ] [ r ] + C r − l i − l ∗ f [ l ] [ i − 1 ] ∗ f [ i + 1 ] [ r ] f[l][r] = f[l][r] + C_{r-l}^{i-l} * f[l][i-1] * f[i + 1][r] f[l][r]=f[l][r]+Crlilf[l][i1]f[i+1][r],我们可以将 f [ l ] [ r ] f[l][r] f[l][r] 看做是若干个删除排列的集合。那么这个式子相当于是把 f [ l ] [ i − 1 ] f[l][i-1] f[l][i1] 对应的删除排列集合中每一个元素(是一个排列)与 f [ i + 1 ] [ r ] f[i + 1][r] f[i+1][r] 集合中的每一个元素拿出来,在不改变两个元素中的 删除位置的相对顺序 的情况下,将两个排列离散后插入 r − l r-l rl个格子里,然后把 i i i 放在最后。这样做是一定 合法 并且 不会重复的。 至于为什么不漏?可以考虑我们对于每一个区间 [ l , r ] [l, r] [l,r],都把所有可能放在最后的位置都考虑了,这样是肯定不会漏的。


三.最短路

题面:
在这里插入图片描述

分析:
         我们考虑如果没有额外的边该怎么算: 可以列出式子 r e s = 2 ∗ n ∗ n ∗ ∑ i = 1 n ∑ j = 1 n ∣ i − j ∣ 2 = n ∗ n ∗ ∑ i = 1 n ∑ j = 1 n ∣ i − j ∣ = n 5 − n 3 3 res =2*\frac{n*n*\sum_{i=1}^{n}\sum_{j=1}^{n}|i-j|}{2}=n*n*\sum_{i=1}^{n}\sum_{j=1}^{n}|i-j|=\frac{n^5-n^3}{3} res=22nni=1nj=1nij=nni=1nj=1nij=3n5n3。不是很难想。

         那么,这些额外的边实际上就是让某些点对之间的距离减小了,用算出来的答案减去 减小的距离和 就是最终答案。

         我们考虑怎样计算减小的距离和:首先我们考虑将额外的边分成两类 斜向右( \ ) 的 和 斜向左( / ) 的,容易想到这两类边不会相互影响(因为任意点对之间的最小距离不可能 由同时经过这两类边 得到),所以我们考虑将分别计算这两类的边的贡献。又因为斜向左的边可以对称变成斜向右的边,因此我们可以将网格图翻转后计算两次即可。下面只考虑如何计算斜向右的边。

         我们先考虑如果枚举一起点,怎样计算它与其他点对之间减小的距离和。
在这里插入图片描述
         如上图,假设 p p p 是我枚举的起点,那么实际影响减小的距离的边是 棕色方框 内的边,它们的起点都在棕色方框内。而红色矩形的图形并的面积减去绿色矩形的面积,就是和 p p p 的距离减小 1 1 1 的点的数量,而绿色矩形的面积就是和 p p p 的距离减小 2 2 2 的点的数量。 所以红色矩形的并的面积 与绿色矩形的面积 之和就是总共减小的距离。这告诉我们 边的终点才是求减少距离的关键

         我们考虑 如果枚举起点该怎么求 不同颜色的矩形的并 的面积之和(即减少的距离)。
         我们预处理出来一个 d i s dis dis 数组, d i s [ i ] [ j ] dis[i][j] dis[i][j] 表示从 i i i 这条边的起点到 j j j 这条边的终点最多经过几条边(不算第 i i i 条边)。那么如果 d x [ i ] ≤ u x [ j ] dx[i] \leq ux[j] dx[i]ux[j] d y [ i ] ≤ u y [ j ] dy[i] \leq uy[j] dy[i]uy[j],那么 d i s [ i ] [ j ] dis[i][j] dis[i][j] 的初值为 1 1 1。( d x [ i ] , d y [ i ] , u x [ i ] , u y [ i ] dx[i],dy[i],ux[i],uy[i] dx[i]dy[i]ux[i]uy[i] 分别表示第 i i i 条边的下端点的坐标和上端点的坐标)。接着,我们跑 f l o y d floyd floyd ,把所有的 d i s [ i ] [ j ] dis[i][j] dis[i][j] 都更新出来,时间复杂度是 O ( m 3 ) O(m^3) O(m3)
        设 f [ i ] f[i] f[i] 表示 i i i 条边的下端点与矩形边缘围城的矩形内的点与起点之间可经过几条边。那么 f [ i ] f[i] f[i] 可以通过枚举棕色矩形内的边用 d i s dis dis 数组更新,求出 f f f 数组的时间复杂度为 O ( m 2 ) O(m^2) O(m2)。然后面积可以 O ( m ) O(m) O(m) 求出(具体方法参考代码),因为可以通过固定行后倒序枚举列来枚举起点,这样对于每一行中的所有起点而言,我们总共只需要将所有的边枚举一次。 因此,我们求出全部点对的答案的复杂度就是 O ( m 3 + n m 2 + n 2 m ) O(m^3 + nm^2 +n^2m) O(m3+nm2+n2m) ,显然是不可以通过的。我们考虑如何优化。
在这里插入图片描述

        在上图中,每一个红色小矩形中任意一个点作为起点求出的答案显然都是一样的,我们考虑将每一条边的横坐坐标离散化后构成这样一个网格图,求出网格图中每一个交点对应的答案,再乘上网格中的点数即可。这样 n n n 就变成了和 m m m 一个量级,时间复杂度 O ( m 3 ) O(m^3) O(m3)代码有亿点细节
CODE

#include<bits/stdc++.h>//将 “\” 和 “/” 分开算 
using namespace std;//考虑容斥   res = 总 - 贡献 - 贡献 
const int N = 510;
typedef long long LL;
int m;
LL n, mod = 998244353, a, b, c, d;
LL T, inv = 332748118;
struct P{//结构体里面算, 简化代码 
	LL ux[N], uy[N], dx[N], dy[N];//存储边的上下端点的横纵坐标
	LL posx[N * 2], posy[N * 2], h[N], f[N], dis[N][N], res;//pos数组用来反向映射    dis数组用来计算从某一条边的起点到另一条边的终点所经过的最大边数  h, f数组用来计算 
    int id[N];
	map< LL, int > mpx, mpy; 
	int tot, lenx, leny, nx, ny;//边的数量    nx, ny指横纵坐标的数量 
	
	void upsort(){//冒泡排序 
		for(int i = 1; i <= tot; i++){
			for(int j = 1; j < tot; j++){
				if(uy[j] > uy[j + 1]){
					swap(ux[j], ux[j + 1]); swap(uy[j], uy[j + 1]);
					swap(dx[j], dx[j + 1]); swap(dy[j], dy[j + 1]);
				}
			}
		}
		return ;
	}
	
	void downsort(){//冒泡排序 
		for(int i = 1; i <= tot; i++){
			for(int j = 1; j < tot; j++){
				if(dy[id[j]] > dy[id[j + 1]])
				   swap(id[j], id[j + 1]);
			}
		}
		return ;
	}
	
	void solve(){
		posx[++lenx] = 1, posx[++lenx] = n;//把边界加进去
	    posy[++leny] = 1, posy[++leny] = n;//边界这样加是正确的,这样加边界可以覆盖到所有的点 
		for(int i = 1; i <= tot; i++){
			posx[++lenx] = ux[i], posy[++leny] = uy[i];
			posx[++lenx] = dx[i], posy[++leny] = dy[i];
		}
		sort(posx + 1, posx + lenx + 1);
		sort(posy + 1, posy + leny + 1);
		nx = unique(posx + 1, posx + lenx + 1) - (posx + 1);//算出数量 
		ny = unique(posy + 1, posy + leny + 1) - (posy + 1);
		for(int i = 1; i <= nx; i++) mpx[posx[i]] = i;//映射一下 
		for(int i = 1; i <= ny; i++) mpy[posy[i]] = i;//映射一下
		for(int i = 1; i <= tot; i++){
			ux[i] = mpx[ux[i]]; dx[i] = mpx[dx[i]];
			uy[i] = mpy[uy[i]]; dy[i] = mpy[dy[i]];//离散化 
		} 
		upsort();//按照上端点的y坐标排序,从左到右方便用指针加边 
		
		for(int i = 1; i <= tot; i++)
			for(int j = 1; j <= tot; j++)
				if(dx[i] <= ux[j] && dy[i] <= uy[j]) dis[i][j] = 1;
			
        for(int k = 1; k <= tot; k++)// floyd
        	for(int i = 1; i <= tot; i++)
        		for(int j = 1; j <= tot; j++)
        			if(dis[i][k] && dis[k][j]) dis[i][j] = max(dis[i][j], dis[i][k] + dis[k][j]);//不算自己
		
		for(int i = 1; i <= tot; i++) id[i] = i;
		downsort();//对编号进行排序, 按照下端点的y坐标排序,方便计算面积 
		for(int i = nx; i >= 1; i--){//倒序枚举行和列  计算面积 
			memset(f, 0, sizeof(f));//将每一条边下端点的区域被覆盖次数都赋值成0 
			int p = tot;//指针用来加边
			for(int j = ny; j >= 1; j--){
				while(p >= 1 && uy[p] >= j){//有可能加进去 
					if(ux[p] >= i){//在范围里 
						f[p] = 1;//拿p去更新其他边的 f[] 
						for(int k = p + 1; k <= tot; k++){
							if(dis[p][k]){
								f[k] = max(f[k], dis[p][k] + 1LL);
							} 
						}
					}
					p--;
				} 
				//计算面积
				LL ans = 0;
				for(int k = 1; k <= tot; k++) h[k] = n + 1LL;//附上初值 
				for(int k = 1; k <= tot; k++){//要按照 id 顺序枚举  保证下端点 从左往右 
					int now = id[k];
					if(f[now]){//有值说明更新了 
					    if(posx[dx[now]] < h[f[now]]){
					   	   ans = (ans + (h[f[now]] - posx[dx[now]]) * ((n + 1LL) - posy[dy[now]])) % mod;
					   	   h[f[now]] = posx[dx[now]];
					    }
					}
				}
				res = (res + (((ans % mod) * (((posx[i] - posx[i - 1]) * (posy[j] - posy[j - 1])) % mod)) % mod)) % mod;//乘上区域面积 
			}
		}
		return ;
	}
}path1, path2;
int main(){
	freopen("path.in", "r", stdin);
	freopen("path.out", "w", stdout);
	cin >> m >> n;
	T = (((((n * ((((n * n) % mod) * ((n * n) % mod)) % mod)) % mod) - (((n * n) % mod * n) % mod)) % mod + mod) % mod * inv) % mod;//没有边的答案   注意括号!!!!!!! 
	for(int i = 1; i <= m; i++){
		cin >> a >> b >> c >> d;
		if(a > c) swap(a, c), swap(b, d);
		if(b < d){//  "\"
			int k = path1.tot; k++;
			path1.ux[k] = a, path1.uy[k] = b, path1.dx[k] = c, path1.dy[k] = d;
			path1.tot = k;
		}
		else{//  "/"
			int k = path2.tot; k++;
			path2.ux[k] = a, path2.uy[k] = n - b + 1, path2.dx[k] = c, path2.dy[k] = n - d + 1;
			path2.tot = k;
		}
	}
	path1.solve(); path2.solve();
	cout << ((T - ((path1.res + path2.res) % mod) % mod) + mod) % mod << endl;//神来之笔
	return 0;
}



四.树

题面:
在这里插入图片描述
在这里插入图片描述
分析:
应该是个树剖,还没写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值