2021.牛客暑假多校 第二场 补题

  1. B题 https://ac.nowcoder.com/acm/contest/11253/B 数学,组合数学

大意: 有一个行数为2 ,列数没有限制的 棋盘,第一行摆了 x 个炮,第二行摆了 y 个炮。一个炮吃掉另一个炮中间当且仅当只有一个炮。设 f k f_k fk为 k 个炮吃炮事件的方案数,分别计算在两种情况下,第一种是两行可以交替发生事件;第二种是必须第一行发生完才能发生第二行,求方案数的异或和。2<=x,y <=5e6。M=1e9+7.

思路:两种情况分开来算,先看第一种情况。 先假设我们有 x 个炮。对于操作一次有多少种方案呢?先考虑中间的炮,每个炮吃两边的 即 2 * (x-4) 再加上 两边的 4个 。所以对于 x个 炮的操作一次的方案为

2 * (x-2) 。 吃掉了一个炮,下一次操作相当于 对 x-1 个炮进行操作。

所以操作 i 次的方案数即为 :

​ 2 * (x-2) * 2 * (x-3) * … * 2 * ( x-i-1)

= 2 k 2^k 2k​​​ * ( x − 2 ) ! ( x − i − 2 ) ! \frac{(x-2)!}{(x-i-2)!} (xi2)!(x2)!​​​​

对于下面的一行同理。

(1)在第一种情况中,我们在对于两行在任何时候可以任选一行进行操作,是没有限制的,设总共操作了 k 次 ,在第一行操作了 i 次,显然这是个组合问题,为了便于对式子的观察,我们令 n=x-2,m=y-2 。所以第一种情况的计算结果如下:

∑ k = 0 n + m 2 k c ( k , i ) ∑ i = 0 k n ! ( n − i ) ! m ! ( m − ( k − i ) ) ! \displaystyle \sum^{n + m}_{k = 0} {2^k c(k,i)\displaystyle \sum^{ k}_{i = 0} {\frac{n!}{(n-i)!}\frac{m!}{(m-(k-i))!}}} k=0n+m2kc(k,i)i=0k(ni)!n!(m(ki))!m!​ 显然两个连加符号时间复杂度是O( n 2 n^2 n2) 是没办法求的。所以我们来想想看能不能将表达式进一步化简。​​​​​​​​

c ( k , i ) c(k,i) c(k,i) 化成 k ! i ! ( k − i ) ! \frac{k!}{i!(k-i)!} i!(ki)!k!​​ 将分母和后面的组合,原式就变成了:

∑ k = 0 n + m 2 k k ! ∑ i = 0 k c ( n , i ) c ( m , k − i ) \displaystyle \sum^{n + m}_{k = 0} {2^k k! \displaystyle \sum^{ k}_{i = 0} {c(n,i){c(m,k-i)}}} k=0n+m2kk!i=0kc(n,i)c(m,ki)

根据实际意义出发: ∑ i = 0 k c ( n , i ) c ( m , k − i ) \displaystyle \sum^{ k}_{i = 0} {c(n,i){c(m,k-i)}} i=0kc(n,i)c(m,ki) = c ( n + m , k ) c(n+m,k) c(n+m,k)

所以原式可化为 : ∑ k = 0 n + m 2 k k ! c ( n + m , k ) \displaystyle \sum^{n + m}_{k = 0} {2^k k! c(n+m,k)} k=0n+m2kk!c(n+m,k)​这样就可以用 O(n)​ 的时间复杂度计算结果了。

(2)对于第二种情况,我们如果一旦选定在第二行进行操作就不能再在第一行操作了。与第一种情况相似,我们同样设一共操作k次,在第一行操作了了 i 次 ,并且用 n 替换 x-2,m 替换 y-2 。所以第二种情况的表达式与第一种情况唯一不同的一点就是少了组合数,即:

$\displaystyle \sum^{n + m}{k = 0} {2^k \displaystyle \sum^{ k}{i = 0} {\frac{n!}{(n-i)!}\frac{m!}{(m-(k-i))!}}} ​ ​ 时 间 复 杂 度 当 然 也 是 O ( ​​ 时间复杂度当然也是 O( O(n^2$) ,继续想想怎么进行化简。

化简嘛,我们尽量想办法消去某一维枚举,对于上面的表达式,我们已经不能拆出什么东西去重新组合了,所以我们想办法去凑出某些式子去消去 i 。分子分母同时乘以 (n+m-k)!。化简得:

$\displaystyle \sum^{n + m}{k = 0} {2^k n! m! \displaystyle \sum^{ k}{i = 0} {c(n+m-k,n-i)}} $​

∑ i = 0 k c ( n + m − k , n − i ) \displaystyle \sum^{ k}_{i = 0} {c(n+m-k,n-i)} i=0kc(n+mk,ni) 又可以化成

∑ i = n − k n c ( n + m − k , i ) \displaystyle \sum^{ n}_{i = n-k} {c(n+m-k,i)} i=nknc(n+mk,i)​​​ 接下来就是关键了。我们可以用前缀和的方式去计算结果

令 S(n,m)= ∑ i = 0 m c ( n , i ) \displaystyle \sum^{ m}_{i = 0} {c(n,i)} i=0mc(n,i)

对于 ∑ i = n − k n c ( n + m − k , i ) \displaystyle \sum^{ n}_{i = n-k} {c(n+m-k,i)} i=nknc(n+mk,i)​​ 也就是 S(n+m-k,n) - S(n+m-k,n–1-k)。

接下来看看对于 S(n,m)怎么求:

显然: S(n,m+1)=S(n,m)+c(n,m+1)

​ S(n,m-1)=S(n,m) -c(n,m);

​ S(n+1,m)=2*S(n,m) - c(n,m)(利用公式 c(n,m)= c(n-1,m-1)+c(n-1,m) )

因为原式通过化简变成了只和 k 有关的计算,接下来就可以 O(n)的时间复杂度枚举 k 进行计算了。

另外还有几个小细节:不能无脑开long long 会爆内存。对于异或的结果是不能取模的。快速幂和逆元提前预处理。

具体看代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e7+10,M=1e9+9,NN=1e7;
int inv[N],q[N],fac[N],s1[N],s2[N],n,m,f1[N],f2[N];
int qmi(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1)res=(LL)res*a%M;
		a=(LL)a*a%M;
		b>>=1;
	}
	return res%M;
}
LL c(LL a,int b)
{
	if(a<b)return 0;
	return (LL)fac[a]*inv[b]%M*inv[a-b]%M;
}
void init()
{
	fac[0]=1,q[0]=1;
	for(int i=1;i<=NN;i++)
	{
		fac[i]=(LL)fac[i-1]*i%M;
		q[i]=(LL)q[i-1]*2%M;
	}
	inv[0]=inv[1]=1;
	for (int i = 2;i <= NN;++i ){
        inv[i]=(LL)inv[M%i]*(M-M/i)%M;
    }
    for(int i=1;i<=NN;i++)inv[i]=(LL)inv[i-1]*inv[i]%M;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	init();
    cin>>n>>m;
    n-=2,m-=2;
    for(int k=0;k<=n+m;k++)f1[k]=(LL)q[k]*fac[n+m]%M*inv[n+m-k]%M;
    s1[n+m]=1;
    for(int k=n+m;k>0;k--)s1[k-1]=((LL)(2*s1[k]-c(n+m-k,n))%M+M)%M;
    s2[n-1]=1;
    for(int k=n-1;k>0;k--)
    {
        s2[k-1]=(s2[k]+c(n+m-k,n-k))%M;
        s2[k-1]=(((LL)2*s2[k-1]-c(n+m-k,n-k))%M+M)%M;
    }
    for(int i = n + m; i >= 0; i--) {
        s1[i] = (s1[i] - s2[i] + M) % M;
    }
    LL ans1 = 0, ans2 = 0;
    for(int i = 0; i <= n + m; i++) {
        f2[i] = (LL)q[i] * fac[n] % M * fac[m] % M * inv[n + m - i] % M * s1[i] % M;
    }
    for(int i = 0; i <= n + m; i++) {
        ans1 ^= f1[i];
        ans2 ^= f2[i];
    }
    cout<<ans1<<" "<<ans2<<"\n";

	return 0;
}

总结:注意推组合数的必要技巧。递推求逆元。

  1. C题 https://ac.nowcoder.com/acm/contest/11253/C 思维题,签到,博弈

大意:给定一个 n * m 网格,构成 n * m 个点。每次选择相邻的点连成一条直线,Alice 和 Bob 轮流操作,所有线段不能构成封闭图形,不能操作的判输。1<=n,m<=4

思路:思维题,n * m 个点,不构成封闭图形最多有 n * m - 1条边,并且和怎么连无关。然后判断奇偶就行了。

  1. 模拟题,签到。注意写好 if else 的条件就行了。
  2. F题 https://ac.nowcoder.com/acm/contest/11253/F 解析几何题

大意:给定四个点A,B,C,D,坐标分别为 (x1,y1,z1)(x2,y2,z2)(x3,y3,z3)(x4,y4,z4)。并给定两个数 k1,k2。动点 P 1 P_1 P1​ 满足 | A P 1 AP_1 AP1​|>=| B P 1 BP_1 BP1​|。动点 P 2 P_2 P2​ | C P 2 CP_2 CP2​|>=| D P 2 DP_2 DP2​|。求两个图形相交部分的体积

(0<= x i , y i , z i x_i,y_i,z_i xi,yi,zi<=1000。1<= k 1 , k 2 k_1,k_2 k1,k2<=100)

思路:做题的时候被这道题给吓到了,以为很难,其实仔细分析下来难度并没有想象中的那么大。首先看题目中给定的形式感觉是球。不妨把坐标带入,化简观察下。设 P 1 P_1 P1 坐标(x,y,z)。带入化简得到的解析式如下:

( k 2 − 1 ) x 2 + ( k 2 − 1 ) y 2 + ( k 2 − 1 ) z 2 − 2 ( k 2 x 2 − x 1 ) x − 2 ( k 2 y 2 − y 1 ) y − 2 ( k 2 z 2 − z 1 ) z + k 2 x 2 2 − x 1 2 + k 2 y 2 2 − y 1 2 + k z 2 2 − z 1 2 < = 0 (k^2-1)x^2+(k^2-1)y^2+(k^2-1)z^2-2(k^2x_2-x_1)x-2(k^2y_2-y_1)y-2(k^2z_2-z_1)z+k^2x_2^2-x1^2+k^2y_2^2-y_1^2+kz_2^2-z_1^2<=0 (k21)x2+(k21)y2+(k21)z22(k2x2x1)x2(k2y2y1)y2(k2z2z1)z+k2x22x12+k2y22y12+kz22z12<=0​​​​

同除以 1 + k 2 1+k^2 1+k2​​ 显然是球的表达式。

对于球的一般形式: x 2 + y 2 + z 2 + 2 a x + 2 b y + 2 c z + d = 0 x^2+y^2+z^2+2ax+2by+2cz+d=0 x2+y2+z2+2ax+2by+2cz+d=0

化为标准形式: ( x + a ) 2 + ( y + b ) 2 + ( z + c ) 2 = a 2 + b 2 + c 2 − d (x+a)^2+(y+b)^2+(z+c)^2=a^2+b^2+c^2-d (x+a)2+(y+b)2+(z+c)2=a2+b2+c2d​​

球心 (-a,-b,-c)。 半径 ( a 2 + b 2 + c 2 − d ) \sqrt{(a^2+b^2+c^2-d)} (a2+b2+c2d)

将上面的式子带入就行了。对于另一个求同理。

接下来就是判定求相交部分的体积了。即两个球半径 R1,R2 ,球心之间的距离 dis

分以下几种情况,(1)相切或相离,相交的体积为0 R1+R2>=dis;

​ (2) 内切 R1+dis<=R2或 R2+dis<=R1。

​ (3) 相交 :在草稿纸上简单的画个图就会发现,相交的部分是两个球缺的体积,注意两个球缺的大小不一定相同。对于球缺的体积公式 v= 1 3 p i ( 3 R − H ) H 2 \frac{1}{3}pi(3R-H)H^2 31pi(3RH)H2​ 对于 可以用余弦定理求出角度进而求H。R是球的半径。

总结:对于解析几何体,不要畏难,如果是熟悉的模型可以要试着去做。

  1. G题 https://ac.nowcoder.com/acm/contest/11253/G

大意:有 n 个人 每个人有一个空闲时间 [ a i , b i ) [a_i,b_i) [ai,bi)​ 前闭后开。现要将他们分成确定的 k 组。每组的空闲时间是他们的交集。并且每组中的交集至少为 1 ,且每组至少有一个人。求分好组后各组的空闲时间之和的最大值。(1<=k<=n<=5000)。

思路:首先搞清楚交集的实质,对于多个区间合到一组,除非区间一模一样否则必有损失。所以我们想办法完成分组的同时尽可能减少损失,从而达到空闲时间之和最大的目的。交集以小为主,这时候我们想到对于那些 “大区间”,即存在某些区间是它的子区间的区间。他们是很特殊的。因为已经存在它的子区间了。从减少损失的角度来思考,要么把他单独放到一组,要么把他放到它的子区间那组,并且放到子区间那组是不影响结果的。所以 “大区间对结果的影响是很单一的,很容易更新。” 所以我们可以考虑把所有的“大区间”挑出来,到最后的时候再去考虑它们的影响。我们将剩下的小区间的按照左端点从小到大排序(区间问题的经典操作),显然这时候右端点也是满足从小到大有序排列的,这样我们就可以得到有序的区间序列方便处理。又因为n<=5000。我们考虑用dp来写。

状态表示:f[i,j] 表示将前 i 个区间分成 j 组的最大空闲时间。 显然对于 i<j 的情况是无意义的,因为要求的是最大值。所以我们初始化f为负无穷。

根据最后一步的思想,即根据分到 j-1组的最后一个人的位置划分集合 所以状态转移方程如下:

f [ i , j ] = m a x ( f [ k , j − 1 ] + a [ k + 1 ] . r − a [ i ] . l ) ( j − 1 < = k < = i − 1 , a [ k + 1 ] . r > a [ i ] . l ) f[i,j]=max( f[k,j-1] + a[k+1].r- a[i].l ) (j-1<=k<=i-1, a[k+1].r>a[i].l ) f[i,j]=max(f[k,j1]+a[k+1].ra[i].l)(j1<=k<=i1,a[k+1].r>a[i].l)

如果我们直接暴力枚举的话显然是不行的,时间复杂度为 O( n 3 n^3 n3​)。我们想想怎么优化。动态规划的优化一般都是在暴力的基础上减少那些重复性操作。经常用的优化方式也就是前缀和,单调队列,斜率优化。

对于上面的式子我们稍微整理一下: f [ i , j ] = − a [ i ] . l + m a x ( f [ k , j − 1 ] + a [ k + 1 ] . r ) ( j − 1 < = k < = i − 1 , a [ k + 1 ] . r > a [ i ] . l ) f[i,j]=- a[i].l +max( f[k,j-1] + a[k+1].r) (j-1<=k<=i-1, a[k+1].r>a[i].l ) f[i,j]=a[i].l+max(f[k,j1]+a[k+1].r)(j1<=k<=i1,a[k+1].r>a[i].l)

经过观察我们就会发现对于固定的 j ,我们枚举 k 其实就是个区间最值问题。显然可以通过单调队列优化。这样就可以省掉一维枚举 k 的时间复杂度。又因为还需要同时满足 a[k+1].r > a[i].l 所以单调队列还需要存区间的信息,可以用一个单调队列和一个存所有需要的信息的结构体实现,也可以两个单调队列用同一套首尾指针分开存。还有很多小细节,具体看代码注释:

#include <bits/stdc++.h>
using namespace std;
const int N=5010;
int f[N][N],sum[N],n,k,m;
struct node{
	int l,r,b;
}a[N],b[N];
bool cmp1(node &A,node &B)
{
	if(A.b!=B.b)return A.b>B.b;
	return A.r-A.l>B.r-B.l; // 注意不能写 >= ,不清楚为什么
}
bool cmp2(node &A,node &B)
{
	return A.l<B.l;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		int l,r;
		cin>>l>>r;
		a[i]={l,r};a[i].b=0;
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		{
			if(i==j||a[j].b)continue;  //注意这是个小细节,只用某个小区间去更新大区间
			if(a[i].l<=a[j].l&&a[i].r>=a[j].r)
			{
				a[i].b=1;
				break;
			}
		}

	sort(a+1,a+1+n,cmp1);

	for(int i=1;i<=n;i++)if(a[i].b!=1)b[++m]=a[i];
	sort(b+1,b+1+m,cmp2);
	memset(sum,-0x3f,sizeof sum);//很重要,防止错误更新
	sum[0]=0;
	for(int i=1;i<=n-m;i++)sum[i]=sum[i-1]+a[i].r-a[i].l;

	memset(f,-0x3f,sizeof f);
	for(int i=1;i<=m;i++) // 因为要用到 j-1 所以先把 j=1的 先算好,之后 j 从 2 开始
		if(b[1].r>b[i].l)f[i][1]=b[1].r-b[i].l;

	vector<int> q1(m+1),q2(m+1);//单点队列,一个存最大值,一个存下标处理不合法的区间
	for(int j=2;j<=k;j++) // j固定的时候才能用单调队列优化,所以先枚举 j
	{
		int hh=1,tt=0;
		q1[++tt]=b[j].r;// q1只需要存每个区间的右端点,每次比较只用到右端点
		q2[tt]=f[j-1][j-1]+b[j].r; // q2 存的是状态转移方程中的max( f[k,j-1] + a[k+1].r)
		for(int i=j;i<=m;i++)
		{
			while(hh<=tt&&b[i].l>=q1[hh])hh++; // 把所有不合法弹出队
			if(hh<=tt)f[i][j]=q2[hh]-b[i].l;
			if(f[i][j-1]<=0)continue;  //f[i][j-1]不合法就没有更新队列的必要了
			while(hh<=tt&&q2[tt]<=f[i][j-1]+b[i+1].r)tt--; // 根据状态转移方程 k 是取不到i的,所以在最后将i的加入队列用于 i+1的更新
			q2[++tt]=f[i][j-1]+b[i+1].r;      
			//另外这里有个问题是想了很久的,之前想的是,万一这里入队的到更新i+1的时候是不合法的并且把一部分合法的也弹出队了怎么办?这个问题是不存在的因为这时候用的是b[i+1].r一定是大于更新 i+1 的时候用的“b[i].l”,即b[i+1].l。
			//所以这里入队的在i+1的时候一定是合法的。但是有可能不是最大的,可能存在些之前入队的更大的,但到i+1变的不合法了,是不能直接用队头更新的,这也是就q1的作用。
			q1[tt]=b[i+1].r;
		}
	}
	int ans=0;
	for(int i=1;i<=k;i++)ans=max(ans,f[m][i]+sum[k-i]);//考虑上大区间的影响,对于多出来的大区间直接加到它的子集那组就行了,不影响结果。
	cout<<ans<<"\n";
	return 0;
}
  1. I题 https://ac.nowcoder.com/acm/contest/11253/I bfs

题目比较有意思,不太难,麻烦,另外还有纪录最短路径,所以从前往后纪录路径。

  1. J题 https://ac.nowcoder.com/acm/contest/11253/J 数论、扩展欧拉定理。

大意:给定一个含有 n 个数的集合 S,其中集合中的数 1 < = x i < = 8 e 4 , 1 < = n < = 4 e 4 1<=x_i<=8e4,1<=n<=4e4 1<=xi<=8e41<=n<=4e4​​。现从中挑 k 个数并计算他们的最大公约数。 1 < = k < = m i n ( ∣ S ∣ , 30 ) 1<=k<=min(|S|,30) 1<=k<=min(S,30)​求所有的挑 k 个数的 GCD 的乘积。结果对 m 取模。 1 e 6 < = m < = 1 e 14 1e6<=m<=1e14 1e6<=m<=1e14​。

思路: n 最多是 4e4 从中挑 k 个直接算是非常难算的。但是注意到 x i x_i xi​ 并不大。所以我们可以从质因子的角度求解(经典操作)。首先需要知道的是对于任意一个数都可以用若干个质因子次幂的形式表示。若两个数 x 1 = p 1 a 1 ∗ p 2 a 2 . . . . p k a k x1=p_1^{a1}*p_2^{a2}....p_k^{ak} x1=p1a1p2a2....pkak x 2 = p 1 b 1 ∗ p 2 b 2 . . . . p k b k x2=p_1^{b1}*p_2^{b2}....p_k^{bk} x2=p1b1p2b2....pkbk

则gcd(x1,x2) 其实 也就是 p 1 m i n ( a 1 , b 1 ) ∗ p 2 m i n ( a 2 , b 2 ) ∗ . . . . . . p k m i n ( a k , b k ) p_1^{min(a1,b1)} * p_2^{min(a2,b2)} * ...... p_k^{min(ak,bk)} p1min(a1,b1)p2min(a2,b2)......pkmin(ak,bk)​​​ 对于多个数的 gcd 本质也是这样。所以我们我们可以通过枚举每个质因子对最终结果的贡献去求最终答案。 假设 对于某个质因子 p 我们考虑它对最终结果的贡献。假设最终结果存在 p x p^x px​​ 我们记录个sum(x) 代表次幂>=x 的 个数。显然选 k 个次幂 >=x 的 就是 C(sum(x),k)。但要保证至少有一个是次幂等于 x 的有多少种呢? 很显然把选k个全部都是>x 那部分去掉就行了,也即是去掉全部是 >=x+1 的 即 C ( s u m ( x ) , k ) − C ( s u m ( x + 1 ) , k ) C(sum(x),k)-C(sum(x+1),k) C(sum(x),k)C(sum(x+1),k)​。这么多种选法对最终结果的贡献都是 p x p^x px​ 所以合起来就是 p x ( C ( s u m ( x , k ) − C ( s u m ( x + 1 ) , k ) p^{x(C(sum(x,k)-C(sum(x+1),k)} px(C(sum(x,k)C(sum(x+1),k) 枚举 x。 底数是相同的,乘积的结果就是指数相加,也就是

$\displaystyle \sum^{}_{x = 0} {x (C(sum(x),k)-C(sum(x+1),k)) } $​​​​ 我们把它拆开 即: C ( s u m ( 1 ) , k ) − C ( s u m ( 2 ) , k ) + 2 ∗ C ( s u m ( 2 ) , k ) − 2 ∗ C ( s u m ( 3 ) , k ) + . . . . . . x ∗ C ( s u m ( x ) , k ) − x ∗ C ( s u m ( x + 1 ) , k ) C(sum(1),k)-C(sum(2),k)+2 * C(sum(2),k)-2 * C(sum(3),k)+......x * C(sum(x),k)-x * C(sum(x+1),k) C(sum(1),k)C(sum(2),k)+2C(sum(2),k)2C(sum(3),k)+......xC(sum(x),k)xC(sum(x+1),k)

化简得 C ( s u m ( 1 ) , k ) + C ( s u m ( 2 ) , k ) + . . . . . . + C(sum(1),k)+C(sum(2),k)+......+ C(sum(1),k)+C(sum(2),k)+......+ 到最后一定是加上某个的形式,为什么呢? 因为每一项加的是和前面的减相对应的,如果加的这一项是0那么前面减的那项也是0,相当于最后面的还是加。而且我们发现对于中间某一项不存在的话是可以直接跳过的。经过化简我们发现可以把对于某个质因子可以把次幂的和算出来,然后考虑质因子p,这样代码就好写很多了。这题时间要求还是很严格的,用到的组合数和质数先提前预处理出来。而且用快速幂求结果的次幂是很大的,所以考虑用扩展欧拉定理降幂。所以得算欧拉函数。用欧拉函数我们用已经筛出来的质数直接算节省时间。因为模数最大是1e14,所以我们质数筛到1e7 就可以了。为什么呢?:类比分解质因子,对于任意一个数y最多只包含一个大于 y \sqrt{y} y 的质因子。 接下来就是扩展欧拉定理降幂:

在这里插入图片描述

另外模数是很大的可能会爆long long ,得用__int 128。还有就是进行加法运算的时候的取模可以用减法代替取模。一旦和大于模数就直接减去模数,这样是直接取模运算要快。为什么可以这样做呢?因为取模的原理就是减去若干倍模数剩下的余数。显然可以边算边减。

还有很多小细节具体见代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=80010,maxn=1e7+10;
LL c[N][35],n,p,k,m,phi;
int a[N];
vector<int> pri;
bool st[maxn];
LL gcd(LL a,LL b)
{
    return b ? gcd(a%b,b):a;
}
void init()
{
	for(int i=2;i<=1e7;i++)
	{
		if(!st[i])pri.push_back(i);
		for(int j=0;i<=1e7/pri[j];j++)
		{
			st[i*pri[j]]=1;
			if(i%pri[j]==0)break;
		}
	}
}
LL Phi(LL n)
{
	LL ans=n;
	for(auto &i:pri)
	{
		if(i>n)break;
		if(n%i==0)
		{
			while(n%i==0)n/=i;
			ans=ans/i*(i-1);
		}
	}
	if(n>1)ans=ans/n*(n-1);
	return ans;
}
LL qmi(LL a,LL b)
{
	LL res=1;
	while(b)
	{
		if(b&1)res=(__int128)res*a%p;
		a=(__int128)a*a%p;
		b>>=1;
	}
	return res%p;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	init();  //提前筛好质数。要用到减法取模,组合数等到算出欧拉函数值之后才能算
	int _;
	cin>>_;
	while(_--)
	{
		cin>>n>>k>>p;
		m=0;
		for(int i=1;i<=n;i++)
		{
			int x;
			cin>>x;
			if(x>m)m=x;
			a[x]++;  // 用桶存数据方便后面计算
		}
		phi=Phi(p);
		c[0][0]=1;
		for(int i=0;i<=n;i++)
			for(int j=0;j<=30;j++)
			{
				if(i<j)break;
				if(j==0)c[i][j]=1;
				else c[i][j]=c[i-1][j]+c[i-1][j-1];
				if(c[i][j]>=phi)c[i][j]-=phi; //减法取模,欧拉降幂
			}
		LL ans=1;
		for(auto &x:pri)
		{
			if(x>m)break;
			for(int i=x+x;i<=m;i+=x)a[x]+=a[i];//枚举含x次幂>=1的个数
			LL tmp=c[a[x]][k];
			for(int j=x*x;j<=m;j*=x)
			{
				for(int k=j+j;k<=m;k+=j)a[j]+=a[k];//次幂逐渐增大

				if(a[j]<k)break;
				tmp+=c[a[j]][k];
				if(tmp>=phi)tmp-=phi; 
			}
			if(tmp)ans=(__int128)ans*qmi(x,tmp)%p;
		}
		cout<<ans<<"\n";
        memset(a,0,sizeof a);
	}
	return 0;
}
/*这里是没有考虑底数和p不互质的情况的,因为很多题目都不会卡这个,考虑的会就会t,得用玄学算法pollard_rho 来分解质因数。但是这样也是过了题的,hack 样例	
1
24 7 1038318
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
答案 519160
下面是正确的扩展欧拉定理,但是有点卡常,得加快读*/
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=80010,maxn=1e7+10;
template<typename T>void write(T x)
{
    if(x<0)
    {
        putchar('-');
        x=-x;
    }
    if(x>9)
    {
        write(x/10);
    }
    putchar(x%10+'0');
}

namespace FastIO {
    const int SIZE = 1 << 16;
    char buf[SIZE], str[64];
    int l = SIZE, r = SIZE;
    int read(char *s) {
        while (r) {
            for (; l < r && buf[l] <= ' '; l++);
            if (l < r) break;
            l = 0, r = int(fread(buf, 1, SIZE, stdin));
        }
        int cur = 0;
        while (r) {
            for (; l < r && buf[l] > ' '; l++) s[cur++] = buf[l];
            if (l < r) break;
            l = 0, r = int(fread(buf, 1, SIZE, stdin));
        }
        s[cur] = '\0';
        return cur;
    }
    template<typename type>
    bool read(type &x, int len = 0, int cur = 0, bool flag = false) {
        if (!(len = read(str))) return false;
        if (str[cur] == '-') flag = true, cur++;
        for (x = 0; cur < len; cur++) x = x * 10 + str[cur] - '0';
        if (flag) x = -x;
        return true;
    }
    template <typename type>
    type read(int len = 0, int cur = 0, bool flag = false, type x = 0) {
        if (!(len = read(str))) return false;
        if (str[cur] == '-') flag = true, cur++;
        for (x = 0; cur < len; cur++) x = x * 10 + str[cur] - '0';
        return flag ? -x : x;
    }
} using FastIO::read;
LL c[N][35],n,p,k,m,phi;
int a[N];
vector<int> pri;
bool st[maxn];
LL gcd(LL a,LL b)
{
    return b ? gcd(a%b,b):a;
}
void init()
{
    for(int i=2;i<=1e7;i++)
    {
        if(!st[i])pri.push_back(i);
        for(int j=0;i<=1e7/pri[j];j++)
        {
            st[i*pri[j]]=1;
            if(i%pri[j]==0)break;
        }
    }
}
LL Phi(LL n)
{
    LL ans=n;
    for(auto &i:pri)
    {
        if(i>n)break;
        if(n%i==0)
        {
            while(n%i==0)n/=i;
            ans=ans/i*(i-1);
        }
    }
    if(n>1)ans=ans/n*(n-1);
    return ans;
}
LL qmi(LL a,LL b)
{
    LL res=1;
    while(b)
    {
        if(b&1)res=(__int128)res*a%p;
        a=(__int128)a*a%p;
        b>>=1;
    }
    return res%p;
}
int main()
{
    
    init();  //提前筛好质数。要用到减法取模,组合数等到算出欧拉函数值之后才能算
    int _;
    read(_);
    while(_--)
    {
        read(n);read(k);read(p);
        m=0;
        for(int i=1;i<=n;i++)
        {
            int x;
            read(x);
            if(x>m)m=x;
            a[x]++;  // 用桶存数据方便后面计算
        }
        phi=Phi(p);
        c[0][0]=1;
        for(int i=0;i<=n;i++)
            for(int j=0;j<=30;j++)
            {
                if(i<j)break;
                if(j==0)c[i][j]=1;
                else c[i][j]=c[i-1][j]+c[i-1][j-1];
                while(c[i][j]-phi>=phi)c[i][j]-=phi; //减法取模,欧拉降幂,p,x不一定互质
            }
        LL ans=1;
        for(auto &x:pri)
        {
            if(x>m)break;
            for(int i=x+x;i<=m;i+=x)a[x]+=a[i];//枚举含x次幂>=1的个数
            LL tmp=c[a[x]][k];
            for(int j=x*x;j<=m;j*=x)
            {
                for(int k=j+j;k<=m;k+=j)a[j]+=a[k];//次幂逐渐增大
 
                if(a[j]<k)break;
                tmp+=c[a[j]][k];
                while(tmp-phi>=phi)tmp-=phi;
                if(tmp>=phi&&gcd(p,x)==1)tmp-=phi;//若p,x互质则再减去一个phi
            }
            if(tmp)ans=(__int128)ans*qmi(x,tmp)%p;
        }
        printf("%lld\n",ans);
        memset(a,0,sizeof a);
    }
    return 0;
}

总结:对于最大公约数和最小公倍数的题可以考虑从质因子的角度分析问题,对于指数很大的快速幂可以考虑用扩展欧拉定理降幂。

  1. K 题 https://ac.nowcoder.com/acm/contest/11253/K 思维题,树状数组,拓扑序

大意:现有a序列和b序列,现进行如下程序

在这里插入图片描述

先随机给出 k 个 (x,y) 代表 b[x]=y。问是否能构造出一个合法的序列a,如果能构造出输出序列a,否则输出-1.序列 a 是 1~n的排列 。1<=k<=n<=1e6 。

方法1:树状数组来做。

思路:首先我们想想b序列的实际含义,也就是“该位置前面有多少个小于它的数”的问题。也即是在没有用的过的数中找到第k大的数,显然可以用树状数组+二分来做。对于每个数我们关注的是它前面的部分,所以从后面往前更新,需要依次更新,把后面的数确定了才能去确定前面的数(比赛的时候没有想到这点,没做出),所以我们需要把b序列补充完整。对于b序列,因为构造不出的情况就两种,一是b[i]>i

另一种就是相邻两个 b[i]-b[j]>i-j, 所以一旦b[i]没有值的话,我们令b[i]=b[i-1]+1 是不会影响我们构造结果的。

#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
#define int long long
int tr[N],a[N],n,k,b[N];
bool st[N];
int lowbit(int x)
{
    return x&-x;
}
int query(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i))ans+=tr[i];
    return ans;
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))tr[i]+=y;
}
bool check(int x,int y)
{
    if(query(x)>=y)return 1;
    return 0;
}
void solve()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)add(i,1);
    for(int i=1;i<=k;i++)
    {
        int x,y;
        cin>>x>>y;
        b[x]=y;
    }
    for(int i=1;i<=n;i++)
    {
       if(b[i]>i)
       {
           cout<<"-1"<<"\n";
           return ;
       }
    }
    int p=0;
    for(int i=1;i<=n;i++)
    {
        if(b[i])
        {
            if(b[i]-b[p]>i-p)
            {
                cout<<"-1"<<"\n";
                return ;
            }
            p=i;
        }
    }
      for (int i = 1; i <= n; i++) {   //必须要先把b序列构造出,才能用树状数组找,因为这种题是不能跳着找的
        if (!b[i]) {
            b[i] = b[i - 1] + 1;
        }
    }
    for(int i=n;i>=1;i--)
    {
        if(b[i])
        {
            int y=b[i];
            int l=1,r=n;
            while(l<r)
            {
                int mid=(l+r)/2;
                if(check(mid,y))r=mid; //在没被用过的数中找到第y大的数
                else l=mid+1;
            }
            a[i]=l;add(l,-1);
            st[l]=1;
        }
    }
    for(int i=1;i<=n;i++)cout<<a[i]<<" ";
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    solve();
    return 0;
}

总结:树状数组+二分解决这种题的时候不要从后往前依次去找,不能跳着找。

方法2:拓扑序列

思路:利用树状数组是从后往前反推的话,那么利用拓扑序列就是从前往后推。我们通过模拟栈的过程,并以大小关系连边(我们这里构建的是大数指向小数的有向边),我们是根据模拟的栈构建的有向图,最后根据拓扑序列更新答案。具体见代码:

#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
int stk[N],deg[N],to[N],n,k,a[N],b[N],hh,v[N];
void bfs()
{
    queue<int> q;
    for(int i=1;i<=n;i++)deg[to[i]]++;
    for(int i=1;i<=n;i++)if(!deg[i])q.push(i);
    int p=n;
    while(q.size())
    {
        int u=q.front();
        v[u]=p--;
        q.pop();
        int t=--deg[to[u]];
        if(t==0)q.push(to[u]);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>k;
    for(int i=1;i<=k;i++)
    {
        int x,y;
        cin>>x>>y;
        b[x]=y;
    }
    int f=1;
    for(int i=1;i<=n;i++)
    {
        if(b[i])
        {
            if(b[i]>hh+1)
            {
                f=0;
                break;
            }
            int g=0;
            while(b[i]<hh+1)hh--,g=1;
            if(g)to[stk[hh+1]]=i; // i节点 可能没有栈顶大
        }
        stk[++hh]=i;  //将 i 进栈
        to[stk[hh]]=stk[hh-1]; // i节点 比栈顶大
    }
    if(!f)cout<<-1<<"\n";
    else 
    {
        bfs();//求拓扑序
        for(int i=1;i<=n;i++)cout<<v[i]<<" ";
    }
    return 0;
}
  1. https://ac.nowcoder.com/acm/contest/11253/L L题 图的分块

有点小麻烦,思路并不难,不想补了。就是令 sqr=sqrt(n)。度数>sqr的记为大点,度数<=sqr的记为小点,对于小点暴力枚举,对于大点考虑从步数最大值的角度更新。日后有机会补更,不想写了。。。。。下次一定

总结:图的分块是根据sqr=sqrt(n) 。然后根据点度数分为大点和小点。小点暴力枚举。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值