Bootstrap

7.26 模拟赛总结 [计数dp] [数数]

复盘

7:40 开题

看 T1 发现又是数数?约束似乎比较多,T2 看起来比较可做,T3 应该是什么神奇计数dp,T4 一眼不太可做

推 T1,想到 “区间加” 这种操作其实很难在线性 dp 里维护,很多后效性,很快想到了在差分数组上拆成 +1 -1 的操作,然后 dp 状态里记录前缀 ,以为会了,直接开写了

发现样例没过,仔细手玩后发现题没看清,操作是有先后顺序的,(1,2)(2,2) 与 (2,2)(1,2) 不是同种方案!以为补个阶乘就行了,发现这样会多算,因为 (2,2)(2,2) 还是只有一种

然后就懵逼了,感觉左右端点拆开做的话就没法维护它们不同,笃定差分假了

然后又胡了个神奇钦定+容斥的做法,再次 十分自信 开写,写完一推样例发现假了… 呃呃呃

然后还在思考 T1,到剩下 2h 多的时候发现情况不对,先打暴力吧,十分慌

开 T2 ,一眼想到差分后找一段不互质的数,想想觉得是什么调和级数神奇值域做法,推了推发现避免不开对 O ( 1 e 18 ) O(1e18) O(1e18) 的数进行质因子分解,难蚌

冷静了一下发现按 r r r 往右扫,维护前面一段段 gcd 变化的位置即可,想想了这个做法复杂度最大 n l o g 2 V nlog^2V nlog2V,但 g c d gcd gcd 衰减的速度肯定比 l o g log log 快很多,开写

测大样例发现 910ms ,很极限;又打打表发现上面说的 g c d gcd gcd 变化的段通常都是 5 5 5 左右,觉得应该能很快。但还是决定和个超级快读,结果发现跑的更慢,于是换回一般快读

剩 50min,慌,看 T3 ,发现朴素的式子很好出,决定先打暴力吧

看 T4 ,发现暴力送了 55pts?但应该只打的完 35,写完后回去看 T1 ,拿点暴力

然后就 30pts 进厂了

so,30+70+15+30=145? rk_inf

( ps: T2 后面的 30pts 均只差了 100ms 以内,甚至那发超级快读获得了 85pts 的好成绩

节奏很崩的一场,发现自己很容易被 T1 搞心态,其实第一步差分的想法很接近正解了,但就是顺序问题一时没想清就把这个思路全盘否定了,而且还影响后面心态

另外就是想到做法一定要先手玩样例,最容易浪费时间的地方就是 调细节多的假做法…

还有就是应该先通览题面的,T3 这种我应该比较擅长的推式子最后也被迫放弃

所以以后的策略应该是:通览题面;不急着写代码,码之前一定要谨慎、理清思路;留足时间打暴力,不要看不起暴力

题解

T1

在这里插入图片描述
正解的模型转化更好,将区间左右端点 +1,-1 看作 括号匹配,这样更方便刻画匹配的方案数

f i , j , k f_{i,j,k} fi,j,k 表示 前 i i i 个位置,当前前缀和为 j j j + 1 +1 +1 操作总共用了 k k k

i i i 转移到 i + 1 i+1 i+1 时,假设用 o o o − 1 -1 1 操作,要与前面的 j j j 匹配,由于这 o o o 个右括号都是一样的,但挑出来的 j j j 个左括号不一定,所以乘上 C j o × o ! C_j^o\times o! Cjo×o!;另外还有新匹配好的这 o o o 对插到长度为 k − j k-j kj 的已完成的操作序列中,即:

f i , j , k × C j o × o ! × C k − j + o o → f i + 1 , j − o , k   ,   0 ≤ o ≤ j f_{i,j,k}\times C_j^o\times o!\times C_{k-j+o}^o\to f_{i+1,j-o,k}\ ,\ 0\leq o\leq j fi,j,k×Cjo×o!×Ckj+oofi+1,jo,k , 0oj

但这样会有个问题,我们相当于是认为了 j j j 个待匹配的左括号互不相同,实际上不是(赛时正是卡到这)

先说一种我赛后订题的想法:

担心若干的左括号会影响?我们直接 在加入左括号时提前 除以 p ! p! p! p p p 代表左括号个数,强制认为它们不同额,费用提前计算?

f i , j , k × C j o × o ! × C k − j + o o × 1 p ! → f i + 1 , j − o + p , k + p   ,   0 ≤ o ≤ j , 0 ≤ p ≤ m − k f_{i,j,k}\times C_j^o\times o!\times C_{k-j+o}^o\times \frac{1}{p!}\to f_{i+1,j-o+p,k+p}\ ,\ 0\leq o\leq j,0\leq p\leq m-k fi,j,k×Cjo×o!×Ckj+oo×p!1fi+1,jo+p,k+p , 0oj,0pmk

这样可以获得 65pts 的好成绩(听说实现好有85)

换一种更好的想法:

既然认为 操作有序 ,我们想象成 长度为 m m m 的一个操作序列,初始为空,每次往里面某些位置插入操作,需要考虑顺序

+1 操作看成往里面为空的位置放数,-1 操作看成往现有的未匹配的 +1 里放,匹配上

那么转移就变得简单:

f i , j , k × C j o × C m − k p → f i + 1 , j − o + p , k + p   ,   0 ≤ o ≤ j , 0 ≤ p ≤ m − k f_{i,j,k}\times C_j^o\times C_{m-k}^p\to f_{i+1,j-o+p,k+p}\ ,\ 0\leq o\leq j,0\leq p\leq m-k fi,j,k×Cjo×Cmkpfi+1,jo+p,k+p , 0oj,0pmk

复杂度 O ( n m 4 ) O(nm^4) O(nm4)

接下来的优化,我们考虑一个事情:这两种转移实际上是独立的

先增加多少 + 1 +1 +1 并不会影响 − 1 -1 1 究竟可以与前面多少个 + 1 +1 +1 匹配,也就是说 j j j p p p 的枚举是独立的

那么我们用两个数组:

f 0 f_0 f0 表示 i i i 位置已进行过两种转移,接下来在 i + 1 i+1 i+1 位置进行 + 1 +1 +1 操作

f 1 f_1 f1 表示 i i i 位置进行了 + 1 +1 +1 ,接下来要进行 − 1 -1 1 转移

两步拆开枚举就能优化到 O ( n m 3 ) O(nm^3) O(nm3),又因为这个 m 3 m^3 m3 实际上是 C m 3 C_m^3 Cm3,常数极小,2.5s 可以通过

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 105 , M = 305 ;
const int mod = 998244353 ; 
LL ksm( LL a , LL b ) 
{
	LL res = 1 , t = a % mod ;
	while( b ) {
		if( b&1 ) res = res * t % mod ;
		b = b >> 1 ;
		t = t * t % mod ;
	}
	return res ;
}

int n , m , a[N] , b[N] ;
LL f0[N][M][M] , f1[N][M][M] , C[M][M] ;// 前i个位置,进行 k 次操作,这个位置的sum为j 

int main()
{
	scanf("%d%d" , &n , &m ) ;
	for(int i = 0 ; i <= m ; i ++ ) {
		C[i][0] = 1 ;
		for(int j = 1 ; j <= i ; j ++ ) {
			C[i][j] = ( C[i-1][j] + C[i-1][j-1] ) % mod ;
		}
	}
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &a[i] ) ;
	}
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &b[i] ) ;
	}
	f0[0][0][0] = 1 ;
	for(int i = 0 ; i <= n ; i ++ ) {
		for(int k = 0 ; k <= m ; k ++ ) {
			// 注意到两种操作其实是独立的,可以分开做 
			for(int j = a[i] ; j <= min(k,b[i]) ; j ++ ) {
				// i,0 -> i+1,1
				if( f0[i][k][j] ) {
					for(int o = 0 ; o <= j ; o ++ ) { // i+1 位置进行 -1 操作,与前面匹配
						f1[i+1][k][j-o] = ( f1[i+1][k][j-o] + f0[i][k][j]*C[j][o] ) % mod ;
					}
				}
			}
			for(int j = 0 ; j <= b[i] ; j ++ ) { // 在 i+1 位置进行 p 次 +1 操作 
				// 0 -> 1
				if( f1[i+1][k][j] ) {
					for(int p = max(0,a[i+1]-j) ; p <= min(m-k,b[i+1]-j) ; p ++ ) { 
						f0[i+1][k+p][j+p] = ( f0[i+1][k+p][j+p] + f1[i+1][k][j]*C[m-k][p] ) % mod ;
					}
				}
			}
		}
	}
	printf("%lld" , f0[n+1][m][0] ) ;
	return 0 ; 
}

T2

在这里插入图片描述
首先贴一个来自 tbls 的逆天 GCD,加上后吊打正解 (bushi

inline LL qGcd(LL a, LL b) {
    if (!a | !b) {
        return a | b;
    }
    int az = __builtin_ctzll(a), bz = __builtin_ctzll(b), z = min(az, bz);
    a >>= az; b >>= bz;
    while (a != b) {
        LL diff = b - a; az = __builtin_ctzll(diff);
        b = min(a, b); a = abs(diff) >> az;
    }
    return a << z;
}

下面说正解:

baka’s trick 佬のblog

首先我赛时有点唐,编了个不常规但更快的做法 (bushi

正常的想法:同样是按 r r r 枚举,实时维护最靠左的 gcd 不为 1 的位置(而不用维护内部的所有 gcd),实际上是双指针

但 gcd 的限制在于 不可撤销,由于本质是 M i n Min Min 操作,我们 l + + l++ l++ 后没法实时维护出区间 gcd

就像回滚莫队之于普通莫队,这个 trick 使得双指针可以便捷维护这类不可撤销信息

动态维护 l , r , m i d l,r,mid l,r,mid 三个指针,对于 [ l , m i d ] [l,mid] [l,mid] 内我维护从 m i d mid mid l l l 的后缀 gcd,从 m i d + 1 mid+1 mid+1 r r r 维护 前缀 gcd,区间 gcd 即可以用 g c d ( f [ l ] , f [ r ] ) gcd(f[l],f[r]) gcd(f[l],f[r]) 得到

r r r 右移时,更新 f [ r ] = g c d ( f [ r − 1 ] , b [ r ] ) f[r]=gcd(f[r-1],b[r]) f[r]=gcd(f[r1],b[r]) ( m i d + 1 < r ) (mid+1< r) (mid+1<r),然后当区间 gcd = 1 时,移动 l l l

l > m i d l>mid l>mid,此时不能再移,那么把 m i d mid mid 移动到 r r r,然后重构 [ l , m i d ] [l,mid] [l,mid] 段内的后缀 gcd,同时更新最靠左的 l l l

	int l = 1 , mid = 1 , ans = 1 ;
	for(int r = 1 ; r < n ; r ++ ) {
		if( r == 1 || mid+1 == r ) f[r] = b[r] ;
		else f[r] = Gcd( f[r-1] , b[r] ) ;
		while( l <= mid && Gcd(f[r],f[l]) == 1 ) l ++ ;
		if( l > mid ) { // 重构 
			mid = r ;
			f[mid] = b[r] ;
			if( b[r] == 1 ) {
				l = r+1 ;//相当于 k=r
			}
			else {
				for(int k = r-1 ; k >= l ; k -- ) {
					f[k] = Gcd( f[k+1] , b[k] ) ;
					if( f[k] == 1 ) {
						l = k+1 ;
						break ;
					}
				}
			}
		}
		ans = max( ans , r-l+2 ) ;
	}

时间复杂度,由于三个指针移动次数是 O ( n ) O(n) O(n),重构次数也是 O ( n ) O(n) O(n),对于这道题复杂度为 O ( n l o g V ) O(nlogV) O(nlogV)

T3

在这里插入图片描述
式子题

本题等价于求:

C t x + C 2 t x + C 3 t x . . . + C n t x C_t^x+C_{2t}^x+C_{3t}^x...+C_{nt}^x Ctx+C2tx+C3tx...+Cntx

看到这个形式,容易想到当 t = 1 t=1 t=1 时,是组合数的下指标求和,我们是会做的:

C 1 x + C 2 x + C 3 x + . . . + C n x = C n + 1 x + 1 C_1^x+C_2^x+C_3^x+...+C_n^x=C_{n+1}^{x+1} C1x+C2x+C3x+...+Cnx=Cn+1x+1

这个式子的组合意义是: n + 1 n+1 n+1 个人中选 x + 1 x+1 x+1 个,枚举选择的人中最大的编号,接下来再在前面选 x x x

如果是 t = 2 t=2 t=2 呢?我们凑一下

∑ i = 1 n C 2 i x + ∑ i = 1 n C 2 i + 1 x = ∑ i = 1 2 n + 1 C i x − C 1 x \sum_{i=1}^n C_{2i}^x+\sum_{i=1}^n C_{2i+1}^{x}=\sum_{i=1}^{2n+1}C_i^x-C_1^x i=1nC2ix+i=1nC2i+1x=i=12n+1CixC1x ,可以直接算

推广一下:

∑ i = 1 n C t i x + ∑ i = 1 n C t i + 1 x + . . . + ∑ i = 1 n C t i + t − 1 x = ∑ i = 1 t n + t − 1 C i x − ( C 1 x + . . . + C t − 1 x ) \sum_{i=1}^n C_{ti}^x+\sum_{i=1}^n C_{ti+1}^x+...+\sum_{i=1}^n C_{ti+t-1}^x=\sum_{i=1}^{tn+t-1}C_i^x-(C_1^x+...+C_{t-1}^x) i=1nCtix+i=1nCti+1x+...+i=1nCti+t1x=i=1tn+t1Cix(C1x+...+Ct1x),可以算

我们设 f ( x , r ) = ∑ i = 1 n C t i + r x f(x,r)=\sum_{i=1}^n C_{ti+r}^x f(x,r)=i=1nCti+rx,已知 ∑ i = 0 t − 1 f ( x , i ) \sum_{i=0}^{t-1} f(x,i) i=0t1f(x,i)

接下来再找一些关系,可以发现相邻两项做差可以用组合数性质,即

f ( x , r ) − f ( x , r − 1 ) = f ( x − 1 , r − 1 ) f(x,r)-f(x,r-1)=f(x-1,r-1) f(x,r)f(x,r1)=f(x1,r1)

f ( x , r ) f(x,r) f(x,r) 全部化成 f ( x − 1 , r ) f(x-1,r) f(x1,r) f ( x , 0 f(x,0 f(x,0) 的形式,带入到 ∑ f \sum f f 的式子即可用 x − 1 x-1 x1 的所有 求出 ( x , 0 ) (x,0) (x,0),然后递推求 ( x , r ) (x,r) (x,r)

复杂度 O ( n t 2 ) O(nt^2) O(nt2)

T4

神奇数数

传送门

在这里插入图片描述
我们考虑对于每个连通块找到一个 “代表元”,以此来计数

让一个连通块中 a i + b j a_i+b_j ai+bj 最小的,如果相同取 i i i 更小,如果仍相同取 j j j 更小的作为代表元,数这样的点的数量就是答案

考虑怎样的 ( i , j ) (i,j) (i,j) 可以当作代表元,首先显然有 a i + b j ≤ x a_i+b_j\leq x ai+bjx

( n n n 作为行, m m m 作为列 ) 接下来找到 ( i , j ) (i,j) (i,j) 左侧第一个 ≤ b j \leq b_j bj 的,右侧第一个 < b j <b_j <bj 的,上方第一个 ≤ a i \leq a_i ai,下方第一个 < a i <a_i <ai,因为这些点是可能 “替代” 这个代表元的

给出一个结论, ( i , j ) (i,j) (i,j) 为代表元,等价于 ( i , j ) (i,j) (i,j) 只走黑块,不能往上、下、左、右走到对应的这四个点中的一个上

充分性:若 ( i , j ) (i,j) (i,j) 为代表元,一旦能走到,说明 ( i , j ) (i,j) (i,j) 与这四个点连通,就会导致 ( i , j ) (i,j) (i,j) 不如这四个点,代表元被替代

在这里插入图片描述

必要性:只要走不到这四个点,就一定走不出这个矩形,因为 往上下左右直着走 是最容易走出去的路径 ( 从大小关系容易得出 ) ,那么 ( i , j ) (i,j) (i,j) 所在的连通块就不与外界连通,而这个矩形内没有点比 ( i , j ) (i,j) (i,j) 作为代表元更优,所以 ( i , j ) (i,j) (i,j) 就是这个连通块的代表元

a i a_i ai 为例,看 ( i , j ) (i,j) (i,j) 走不到四个点等价于什么:

设左边第一个比 a i a_i ai 大的位置为 L i L_i Li,右边为 R i R_i Ri,那么:

m i n (   m a x ( a L i , . . . a i ) , m a x ( a i , . . . , a R i )   ) + b j > x min(\ max(a_{L_i},...a_i),max(a_i,...,a_{R_i})\ )+b_j > x min( max(aLi,...ai)max(ai,...,aRi) )+bj>x

记前面那一坨为 A i A_i Ai , A i A_i Ai 显然可以 O ( n ) O(n) O(n) 求出,同理定义 B i B_i Bi,那么合法的代表元 ( i , j ) (i,j) (i,j) 满足:

{ a i + b j ≤ x A i + b j > x a i + B j > x \left\{\begin{matrix} a_i+b_j\leq x \\A_i+b_j>x \\a_i+B_j>x \end{matrix}\right. ai+bjxAi+bj>xai+Bj>x

显然这是一个偏序问题,二维数点扫描线即可

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 2e5+100 , INF = 2e5 ;

// 神仙题 
int n , m , X , a[N] , b[N] ;
int sta[22][N] , stb[22][N] , Lg2[N] ;
void make_st()
{
	for(int i = 2 ; i <= max(n,m) ; i ++ ) Lg2[i] = Lg2[i/2] + 1 ;
	for(int i = 0 ; i <= 20 ; i ++ ) {
		if( i == 0 ) {
			for(int j = 1 ; j <= n ; j ++ ) sta[i][j] = a[j] ;
			for(int j = 1 ; j <= m ; j ++ ) stb[i][j] = b[j] ;
		}
		else {
			for(int j = 1 ; j <= n-(1<<i)+1 ; j ++ ) {
				sta[i][j] = max( sta[i-1][j] , sta[i-1][j+(1<<(i-1))] ) ;
			}
			for(int j = 1 ; j <= m-(1<<i)+1 ; j ++ ) {
				stb[i][j] = max( stb[i-1][j] , stb[i-1][j+(1<<(i-1))] ) ;
			}
		}
	}
}
int querya( int l , int r )
{
	int k = Lg2[r-l+1] ;
	return max( sta[k][l] , sta[k][r-(1<<k)+1] ) ;
}
int queryb( int l , int r )
{
	int k = Lg2[r-l+1] ;
	return max( stb[k][l] , stb[k][r-(1<<k)+1] ) ;
}
int stk[N] , top , A[N] , B[N] ;
struct line
{
	int x , y , f ;
}l[2*N] ;
int len , Min = 1e9 ;
void pre_work()
{
	for(int i = 1 ; i <= n ; i ++ ) {
		while( top && a[i] < a[stk[top]] ) top -- ;
		int l = stk[top]+1 ;
		if( !top ) A[i] = INF ;
		else A[i] = querya( l , i ) ;
		stk[++top] = i ; 
	}
	top = 0 ;
	for(int i = 1 ; i <= m ; i ++ ) {
		while( top && b[i] < b[stk[top]] ) top -- ;
		int l = stk[top]+1 ;
		if( !top ) B[i] = INF ;
		else B[i] = queryb( l , i ) ;
		stk[++top] = i ; 
	}
	top = 0 ;
	stk[0] = n+1 ;
	for(int i = n ; i >= 1 ; i -- ) {
		while( top && a[i] <= a[stk[top]] ) top -- ;
		int r = stk[top]-1 ;
		if( !top ) A[i] = min( A[i] , INF ) ;
		else A[i] = min( A[i] , querya(i,r) ) ;
		stk[++top] = i ;
	}
	top = 0 ;
	stk[0] = m+1 ;
	for(int i = m ; i >= 1 ; i -- ) {
		while( top && b[i] <= b[stk[top]] ) top -- ;
		int r = stk[top]-1 ;
		if( !top ) B[i] = min( B[i] , INF ) ;
		else B[i] = min( B[i] , queryb(i,r) ) ;
		stk[++top] = i ;
	}
	for(int i = 1 ; i <= m ; i ++ ) {
		l[++len] = {X-B[i]+1,X-b[i]+1,1} ;
		l[++len] = {X-b[i]+1,X-b[i]+1,-1} ;
		Min = min ( Min , X-B[i]+1 ) ;
	}
}
int ida[N] ;
bool cmpa( int x , int y )
{
	return a[x]<a[y] ;
}
bool cmpb( line x , line y )
{
	return x.x < y.x ;
}
struct BIT
{
	int t[2*N] ;
	inline int lowbit( int x ) { return x&-x ; }
	void add( int p , int x ) { // 后缀 
		for( ; p <= INF ; p += lowbit(p) ) t[p] += x ;
	}
	int ask( int p ) {
		int res = 0 ;
		for( ; p ; p -= lowbit(p) ) res += t[p] ;
		return res ;
	}
} T ;
LL ans ;

int main()
{
	scanf("%d%d%d" , &n , &m , &X ) ;
	for(int i = 1 ; i <= n ; i ++ ) scanf("%d" , &a[i] ) , ida[i] = i ;
	for(int i = 1 ; i <= m ; i ++ ) scanf("%d" , &b[i] ) ;
	make_st() ;
	pre_work() ;
	sort( ida+1 , ida+n+1 , cmpa ) ;// query
	sort( l+1 , l+len+1 , cmpb ) ;// modify
	for(int r = Min , i = 1 , j = 1 ; r <= INF ; r ++ ) {
		while( j <= len && l[j].x == r ) {
			T.add( max(1,l[j].y) , l[j].f ) ;
			j ++ ;
		}
		while( i <= n && a[ida[i]] == r ) {
			ans += T.ask( A[ida[i]] ) ;
			i ++ ;
		}
	}
	printf("%lld" , ans ) ;
	return 0 ; 
}
;