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}⌋}\) 约等于 \(n\sum_{i=1}^{n}\frac{1}{i}\)
时间复杂度为调和级数 \(O(n\log{n})\)
关于调和级数:
\(\sum_{i=1}^{n}\frac{1}{i}\) 是调和级数。
所以总复杂的为 \(O(n\log{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*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\n");          //判断能否将路径修正为L,即判断gcd是否为L的因数 
		else printf("NO\n");
	}
	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^{k\phi(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\n",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(n\log{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\n",ans);
	return 0;
}
posted @ 2020-11-01 20:16  暗い之殇  阅读(103)  评论(0编辑  收藏  举报