CSP-S2考前周末刷题班Day5

大大大

(Illyasviel):"两个数乘起来会比一个数大吗?"

(Star-dust):"不知道啊,来算算吧。"

读入一个 (n),对于一个三元组 ((i,j,k)) 满足要求当且仅当(1≤i,j,k≤n)(i×j≥k)

输入描述:

一行一个数字 (n)

输出描述:

一行一个 (ans) 表示满足要求的三元组的个数。

输入样例:

10

输出样例:

900

数据范围:

对于 (30\%) 的数据 (n≤100)
对于 (60\%) 的数据 (n≤5000)
对于 (100\%) 的数据 (n≤100000)

Solution

30pts

(n<=100,O(n^3)) 即可满足需求。
分别枚举 (i,j,k) 是什么,然后判断是否合法。

60pts

(n<=5000)
考虑对于一组确定的 (i,j),满足要求的 (k) 的个数为 (min(n,i*j))
考虑在枚举 (i,j)(O(1)) 的时间内求一个可行区间。
(O(n^2)) 的时间内能解决问题。

100pts

(n<=100000)
我们来反向考虑有多少个不满足要求的三元组,即 (i*j<k)
那么我们考虑将 ((i,j)) 插入 (i*j+1) 中,即因为 (i*j<k),所以在 ([i*j+1,n]) 这个区间的 (k) 都是满足条件的,然后求一个前缀和即可。
复杂度分析:
(i*j>n) 时,直接 (break)
对于一个 (i)(⌊frac{n}{i}⌋) 个满足要求的 (i) 使得 (i*j<n) 总有效点对个数为 (sum_{i=1}^{n}{⌊frac{n}{i}⌋}) 约等于 (nsum_{i=1}^{n}frac{1}{i})
时间复杂度为调和级数 (O(nlog{n}))
关于调和级数:
(sum_{i=1}^{n}frac{1}{i}) 是调和级数。
所以总复杂的为 (O(nlog{n}))
调和级数估计的简略证明:
(1+frac{1}{2}+...+frac{1}{2^n-1})
考虑分组
({1},{frac{1}{2},frac{1}{3}},{frac{1}{4},frac{1}{5},frac{1}{6},frac{1}{7}}...{frac{1}{2^{n-1}}...frac{1}{2^n-1}})
每一组的和小于 (1)
当项数为 (2^n-1) 时和小于 (n),当项数为 (n) 时和的级别小于 (log{n})

关于 (O(n)) 的算法是 (60pts) 算法的优化:
(60pts)(n^2) 枚举写出来:(sum_{i=1}^{n}sum_{j=1}^{n}min(n,i*j))
发现 (j) 的上界是 (frac{n}{i}),且 (i*j) 的变化由 (1*i->2*i->...->frac{n}{i}*i),那么 (i) 所能带来的贡献为:((n-1*i)+(n-2*i)+(n-frac{n}{i}*i)),发现这个是一个等差数列求和,可以 (O(1)) 算出。

关于 (O(sqrt{n})) 的算法是用整数分块做的,不过由于这只是 (T1),就不多做介绍了。

#include<bits/stdc++.h>
#define N 120000
using namespace std;
long long a[N];
long long b[N];
long long ans=0;
long long n;
int main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n/i;j++)
		{
			a[j*i]++;
		}
	}
	for(int i=1;i<=n;i++) b[i]=b[i-1]+a[i-1];
	for(int i=1;i<=n;i++) ans+=b[i];
	printf("%lld
",n*n*n-ans);
}

kkk

(Star-dust):"你会最短路吗?"

(Illyasviel):"当然!"

(Star-dust):"那你来求一求这 (k) 个关键点中是否存在长度 (\%P)(L) 的路径吧。"

(Illyasviel):"这和最短路有什么关系吗?"

(Star-dust):"不知道啊~"

输入描述:

​ 第一行一个数字 (T) 代表数据组数。

​ 对于每个数据,第一行五个数 (n,m,k,P,L​) ​表明有 (n) 个点,(m) ​条边,(k) ​个在图中的点。

​ 接下来一行 (k) 个数代表关键点。

​ 接下来 (m) 行每行三个数 (x,y,z) 表示 (x)(y) 之间有一条长度为 (z) 的路径。(图为无向联通图)

输出描述:

​ 输出 (T) 行,当存在一条路径从起点和终点都在 (k) 个点中输出 "(YES)",否则输出 "(NO)"(不包含引号)。

输入样例:

1
2 2 2 5 3
1 2
1 2 1
2 1 1

输出样例:

YES

样例解释:

(1->2->1->2)

数据范围:

对于 (40\%) 的范围 (T≤500,0≤L,z≤P≤20,k≤n≤m≤500,k≤10)
对于 (80\%) 的范围 (T≤500,0≤L,z≤P≤20,k≤n≤m≤500)。​​
对于 (100\%) 的范围 (T≤500,0≤L,z≤P≤10^9,k≤n≤m≤500,P) ​是奇数。

Solution

40pts

设置 (f[i][j][o]) 表示从第 (i) 个点出发,目前在第 (j) 个点,路径长度 (\%P=o),是否能达到。
跑一遍 (bfs) 或者记忆化 (dfs) 来枚举整张图能否达到。
如果存在了已经满足的话就退出。
复杂度为 (O(T*n*m*k*L))。 加一点 (break) 剪枝能过。

80pts

考虑从任意一个点 (o) 出发,如果有两条路径能分别达到能到达两个关键点 (x)(y),他们的长度分别为 (A)(B),那么就会存在一条长度为 (A+B) 的路径。
如果存在一条路径 (x->z->y) 使得条件满足,可以通过反复走 (2P)(z->o)(o->z) 的路径长度和为 (0)
就是通过 (o) 的路径能满足所有长度的可行路径。
只搜一个点即可。
(f[i][k]) 表示从第 (o) 个点到达 (i) 点路径长度 (\%P=k) 是否存在。

100pts

我们考虑从若存在一条长度为 (l) 的边,走过去走回来的长度为 (2*l),我们可以反复走这条边,可以走 (k*2*l)
那么它在 (\%P) 意义下等价于 (gcd(l,P)),如果我们有一条长度为 (x) 的路径经过这路径两端中的一个,那么就可以通过反复走这条边达到(k*gcd(x,l,P)) 的全部长度。

证明:
设通过走若干次这条边所能到达的长度为:(a)
则有:
(k*l≡a(mod P))
(∴k*frac{l}{gcd(l,P)}≡frac{a}{gcd(l,P)}(mod frac{P}{gcd(l,P)}))
(∵frac{l}{gcd(l,P)})(frac{P}{gcd(l,P)}) 互质
(∴frac{a}{gcd(l,P)}) 能表示 (0) ~ (frac{P}{gcd(l,P)}-1) 内所有的数
(∴a) 能表示的数为:(0,1*gcd(l,P),2*gcd(l,P),...,P-gcd(l,P))
(∴a) 能表示 (k*gcd(l,P)) 的任何长度
证毕

现在我们考虑经过多条边 (w_1,w_2,...,w_m),已经每条边各自经过的次数 (k_1,k_2,...,k_m),那么路径会加上 (2*(k_1w_1+k_2w_2+...+k_mw_m)) 的长度。
现在就是判断 (2*(k_1w_1+k_2w_2+...+k_mw_m)) 能否把路径修正为 (L(mod P))
这个所能到达的值域跟原先的路径所能到达的值域是一样的,那么就看它们全部的 (gcd) 是否是 (L) 的因数。
对于怎么保证起点和终点都是关键点呢?假设有个关键点为 (o),我们可以先走这样一条路径:(o->x_1->o->x_2->...->o->x_n->o),就是从 (o) 点开始把每个点都逛一遍再回来,发现这条原始路径也是由 (2*(k_1w_1+k_2w_2+...+k_mw_m)) 所构成的,发现并不影响我们修正。
复杂度 (O(T*m*log{P}))

#include<iostream>
#include<cstdio>
using namespace std;
int T,n,m,K,P,L,u,v,w;
int GCD(int a,int b)
{
	if(b==0) return a;
	return GCD(b,a%b);
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d%d%d%d",&n,&m,&K,&P,&L);
		int gcd=P;
		for(int i=1;i<=K;i++) scanf("%d",&u);  //这K个关键点根本没用 
		for(int i=1;i<=m;i++)
		{
			scanf("%d%d%d",&u,&v,&w);
			if(w==0) w=P;                      //由于是在膜P意义下,我们把长度为0的边修正为P 
			gcd=GCD(gcd,w);                    //对每条边进行gcd 
		}
		if(L%gcd==0) printf("YES
");          //判断能否将路径修正为L,即判断gcd是否为L的因数 
		else printf("NO
");
	}
	return 0;
}

A的B次方

题目描述:

(Illyasviel):"今天我们学了 (A^B)?"

(Star-dust):"我们也学了诶!"

(Star-dust):"那我来考考你吧!"

已知 (A)(P),求任意一个不等于 (A) 且小于 (2×10^{18})(B) 使得 (A^B≡B^A (mod) (P))

输入描述

一行输入两个整数 (A)(P)

输出描述

输出任意一个满足要求的数字 (B)

(B) 要为一个不大于 (2×10^{18}) 的正整数。

样例输入

78 100

样例输出

16

数据范围

对于 (30\%) 的数据:(1≤A,P≤1000)
对于 (30\%) 的数据:(P) 为质数。
对于 (100\%) 的数据:(64≤A≤10^9,P≤10^9,1≤B≤10^{18})

Solution

30pts

(for) 循环暴力枚举,然后快速幂判断。
但实际上这个做法能拿 (50pts)

100pts

我们可以构造一个 (B),使得 (B=A(mod P)),即 (B=k*P+A①)
这样使得 (B^A=A^A(mod P))
我们想办法再构造 (A^B=A^A(mod P))
扩展欧拉定理可得:(A^{kphi(P)+A}=A^A(mod P))
所以可以让 (B=k*phi(P)+A②)
由于我们构造的 (B) 要满足 (①②) 两式,所以我们让 (B=P*phi(P)+A) 即可。
由于 (P) 的范围是 (1e9),所以我们要在 (sqrt{P}) 的时间内求出 (phi(P))
我们把 (P) 写成唯一分解定理的形式:(P=p_1^{k_1}p_2^{k_2}p_3^{k_3}*...*p_n^{k_n})
(phi(P)=P*(1-frac{1}{p_1})*(1-frac{1}{p_2})*(1-frac{1}{p_3})*...*(1-frac{1}{p_n}))
所以我们可以去枚举 (P) 的每个质因子,枚举到一个就乘一下这个质因子的贡献 ((1-frac{1}{p_i})) 即可。
时间复杂度 (O(sqrt{P}))

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<queue>
#define ll long long
using namespace std;
inline int read()
{
	char ch=getchar();
	int a=0;
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') a=(a<<1)+(a<<3)+(ch^48),ch=getchar();
	return a;
}
ll A,P;
int main()
{
	A=read();P=read();
	ll phi=P,p=P;              //phi就是我们要求的 φ(P) 
	for(ll i=2;i*i<=P;i++)     //枚举P所有的质因子 
	{
		if(P%i==0)             //由于唯一分解定理可知,我们所能找到的因数i一定是P的质因子 
		{
			phi=phi/i*(i-1);   //找到一个质因子,就根据公式计算一下phi 
			while(P%i==0) P/=i;//注意这里我们要把P中所有的质因子i全部除去,以保障答案不会重复计算和i为质因子 
		}
	}
	if(P>1) phi=phi/P*(P-1);   //如果还剩了最后一个质因子,别忘了算上 
	printf("%lld
",A+p*phi);  //输出A+P*φ(P) 
	return 0;
}

灯塔

(Star-dust):"每个人都是灯塔,灯塔之间相隔万里,无法触碰无法沟通,唯一能做的就是用自己的光去照耀别人。"

(Illyasviel):"如果能被某个灯塔一直照耀,那一定很幸福吧。"

(Star-dust):"我能成为你的灯塔吗?"

(Illyasviel):"好啊~"

海上有着 (n) 个灯塔,第 (i) 个灯塔在位置 (i) 闪耀着,灯塔的光覆盖着 ([i−d_i,i+d_i]) 的所有灯塔,对于第 (k) 个灯塔,他想知道有多少个 (i) 满足 (i<k) 且至少存在一个在 (i)(k) 中间的灯塔 (j) 满足灯塔 (j) 同时被灯塔 (i) 和灯塔 (k) 照耀,并且 (j)(k) 的距离小于等于 (j)(i) 之间的距离。

输入描述:

第一行一个整数 (n)

接下来一行 (n) 个数字,第 (i) 个代表 (d_i)

输出描述:

一行一个答案 (ans)

(f_k) 表示对于第 (k) 个灯塔有多少个灯塔满足条件。

(ans)(n)(f_k) 的异或和。

样例输入:

10
2 2 3 2 3 2 3 3 3 1

样例输出:

2

样例解释:

对应位置答案分别为 (0 ,0 ,1 ,2, 3, 3, 3, 4, 4, 2)

数据范围:

对于 (20\%) 的数据:(n≤100)
对于 (20\%) 的数据:(n≤5000)
对于 (20\%) 的数据: (d_i) 完全相同。

对于 (20\%) 的数据:(n≤100000)
对于 (100\%) 的数据:(n≤3000000,1≤d_i≤n)

Solution

20pts

(O(n^3)) 暴力枚举三元组判断是否可行。

40pts

考虑一对 (i)(k),判断是否存在满足条件的 (j),使得:
(①:i+d_i<=j)
(②2*j>=i+k)
(③j>=k-d_k)
整理一下:(i+d_i<=j<=k-d_k,2*j<=i+k)
对于一对 (i,k) 有一个 (j) 的可行区间,第二条限制贪心取最右的判断是否满足要求。
时间复杂度 (O(n^2))

60pts

基于 (40pts) 的做法再对 (20\%) 的数据做特判。
(20\%) 的数据的答案属于一个阶梯上升一个平台然后再下降。
能人眼观察的到。

100pts

我们考虑 (j<=i+d_i) 这个限制条件,对于 (i) 来说,(j) 肯定是越大越好,所以让 (j=i+d_i)这样我们对于每一个 (i) 已经确定了一个最优的 (j)
再考虑 (2*j>=i+k) 这个限制条件,我们移一下项:(k<=2*j-i),这样我们就知道了这个 ((i,j)) 最大所能满足的 (k)
再考虑 (k-d_k<=j) 这个限制条件,这一条就是限制了对于 (k) 来说可行 (j) 的范围。
对于一个 (i) 来说,它的最优的 (j) 就是 (i+d_i),它的可行 (k) 的区间范围在 ([i+2,min(2*j-i,n)])
所以说,当 (k) 的值超过了 (2*j-i)((i,j)) 这一对灯塔就不会对 (k) 产生贡献了,且对于更往后的 (k) 也不会产生贡献。
所以我们开一个 (vector) 记录此刻所需要删除的灯塔对 ((i,j))
然后我们以 (k) 为时间线,开一个树状数组记录满足条件的 (i) 的贡献,我们把贡献记录到对应的 (j(j=i+d_i)) 上去。这样做的目的是为了更好的判断符合条件的 (j) 能否被 (k) 照到。
由于树状数组维护的是 (j),所以 (vector) 里光存 (j) 即可。
算法流程:
(k) 不断向右枚举时,增加的可行的 (i=k-2,j=i+d_i),那么将这对 ((i,j)) 加入树状数组中(具体操作为将树状数组中下标为 (j) 的位置数值 (+1))。
同时也要考虑从 (k-1) 转移到 (k) 的过程中,哪些点对 ((i',j')) 由原来的合法变成不合法了,我们已经将此刻新产生的不合法的点对都记录到了 (vector) 里了,我们需要遍历一遍然后将它们都从树状数组中删掉。
然后我们要考虑当前 (k) 的答案是多少,也就是 (k) 所能照到的范围中有几个 (j)。由于前面我们是令 (j=i+d_i),没有规定它的上界,所以这个 (j) 可能会在 (k) 的后面(由于 (k) 随时在变,所以我们并没有让 (j=min(i+d_i,k)),因为这可能会对下一个 (k) 产生影响),但这并不影响我们统计答案(因为在 (k) 后面的 (j) 你都可以把它搞到 (k) 前面,起码能搞到 (k-1) 的位置),我们就做一次后缀和就好了,范围是 ([k-d_k,n])
时间复杂度 (O(nlog{n}))

#include<iostream>
#include<cstdio>
#include<vector>
#define ll long long
using namespace std;
inline int read()
{
	char ch=getchar();
	int a=0,x=1;
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') a=(a<<1)+(a<<3)+(ch^48),ch=getchar();
	return a;
}
const int N=3e6+5;
int n,c[N],d[N];
vector<int> a[N];
ll f[N],ans;
int lowbit(int x)
{
	return x&(-x);
}
void insert(int x,int y)
{
	x=n-x+1;                        //这是树状数组求后缀和的方法,反着记录,这样求得的前缀和就是你要的后缀和  
	for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}
int query(int x)
{
	x=n-x+1;
	int cnt=0;
	for(int i=x;i;i-=lowbit(i)) cnt+=c[i];
	return cnt;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++) d[i]=read();
	for(int k=3;k<=n;k++)           //第1,2个灯塔不存在合法的j,所以不会产生贡献 
	{
		int i=k-2;                  //对于当前的k,新增的i就是k-2 
		int j=min(i+d[i],n);        //j是对于当前i的最优的情况 
		int m=min(2*j-i+1,n+1);     //对于(i,j),当k到了2j-i+1的时候就不合法了,注意这里上界是n+1,表示(i,j)存在永远合法的情况(始终不会被删去) 
		a[m].push_back(j);          //记录当k到了m的时候需要删掉哪些j 
		insert(j,1);                //把新增的合法的(i,j)记录上 
		for(int l=0;l<a[k].size();l++)  //删掉那些不合法的 
	    {
	    	insert(a[k][l],-1);
		}
		f[k]=query(max(k-d[k],1));  //后缀和统计答案 
		ans^=f[k];
	}
	printf("%lld
",ans);
	return 0;
}
原文地址:https://www.cnblogs.com/xcg123/p/13909452.html