各种奇奇怪怪的题目

$$\Huge{各种奇奇怪怪的题目}$$

对于所有的问题,记 \(n\) 为序列长度, 记 \(m\) 为查询次数(如果是多次查询)

1、

区间 \(\gcd\) : 给你一个序列 \(a\), 每次给你一个区间,求这个区间所有数的gcd, \(n\leq3\times 10^5, m\leq10^6, a_i \leq 10^9\)

法一:暴力

时间复杂度 \(O(mn \log V)\) , 空间复杂度 \(O(n)\) ,其中 \(\log V\) 是gcd复杂度, 显然无法通过

法二:线段树

我们发现 \(\gcd(a,b,c,d)=\gcd(\gcd(a,b),\gcd(c,d))\) ,所以考虑用线段树维护

利用这个性质,每次pushup改为gcd即可

时间复杂度 \(O(n \log V + m \log n \log V)\) ,因为常数大所以也无法通过

法三: 人类智慧

根据数学直觉,如果数据不经过精心构造,十几个数gcd起来答案就很可能是 \(1\)

所以我们以 \(40\) 为界,大段直接输出 \(1\) ,小段我们选择暴力处理,实际上得分高于线段树

这里还有一个优化:如果你已经算过这个区间了,那就不用再算了,扔一个 \(map\) 保存答案即可

(小声BB: 这些不都是我经常干的事吗)

法四: ST表

我们又发现这道题他没有修改,因为他有可重复贡献性 \((\gcd(x,x)=x)\),所以可以考虑ST表解决

时间复杂度 \(O(n \log n + n \log V + m \log V)\)

贴代码

#include<bits/stdc++.h>
using namespace std;
const int N=3000005;
inline char gc () {
	static char buf[4000000], *p1 = buf, *p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 4000000, stdin), p1 == p2) ? EOF : *p1 ++ ;
}
int read () {
	int ans = 0; char ch = gc();
	while(!isdigit(ch)) ch = gc();
	while(isdigit(ch)) ans = ans * 10 + ch - '0', ch = gc();
	return ans;
}//题目给的快读板子,码风不同很正常
int log_2[N],n,m,a[N],x,y,ans[N],f[N][25];
long long pow2[55];
int gcd(int a,int b){return b?gcd(b,a%b):a}
int main(){
	n=read();m=read();
	pow2[0]=1;log_2[0]=-1;
	for(int i=1;i<=21;++i)pow2[i]=pow2[i-1]*2;
	for(int i=1;i<=n;++i)log_2[i]=log_2[(i>>1)]+1;
	for(int i=1;i<=n;++i)f[i][0]=read();
	for(int i=1;i<=21;++i){
		for(int j=1;j+pow2[i]-1<=n;++j)
			f[j][i]=gcd(f[j][i-1],f[j+pow2[i-1]][i-1]);
	}
	for(int i=1;i<=m;++i){
		x=read();y=read();
		if(x==y){ans[i]=f[x][0];continue;}
		int logl=log_2[y-x+1];
		ans[i]=gcd(f[x][logl],f[y-pow2[logl]+1][logl]);
		printf("%d\n",ans[i]);
	}
	return 0;
}

2、

\[\sum_{i=1}^{n} \sum_{j=i}^{n}\max(a[i],a[i+1],\ldots,a[j]) \]

\(998244353\) 取模的值, \(n\leq 10^6\)

法一:暴力

不用多说,时间复杂度 \(O(n^3)\)

法二:优化暴力

对法一用ST表优化,使得区间查询复杂度变为常数,复杂度 \(O(n^2)\)

法三:优化暴力2

因为区间每次扩展1,我们发现可以在计算的时候记录这个区间的值,可以在O(n)的时间里求出O(n)个区间的值,复杂度 \(O(n^2)\)

分析到了这步,我们发现这种区间是平方级别的。那真的没有更快的做法了吗?

法四:转化问题

前三个方法都是面向区间单独计算,这里我们转化一下问题

考虑计算每一个数的贡献。

对于一个序列中的数 \(a_i\) ,我们认为包含它的区间中,它是最大值的情况,为避免重复计算,当且仅当它所属的区间左端点的下标大于它左边第一个大于等于它的数的下标,而且它所属的区间右端点的下标小于它右边第一个大于它的数的下标。

所以,对于一个数 \(a_i\) ,我们令他左边大于等于他的下标最大的数为 \(a_l\) , 令他右边大于他的下标最小的数为 \(a_r\)

如果左边没有大于等于他的数,那么 \(l=0\) , 右边没有则 \(r=n+1\)

那么他的贡献就是 \(a[i]\times(r-i+1)\times (i-l+1)\)

可以 \(O(n)\) 计算。

对于 \(a_l\)\(a_r\) 的计算,暴力即可,最坏复杂度为 \(O(n^2)\),但是随机数据下可以吊打法二和法三

法五:正解

对于法四的 \(a_l\)\(a_r\) 的计算,考虑单调栈优化。每个下标入栈、出栈各一次,时间复杂度 \(O(n)\)

上代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int mod=998244353;
inline void IO(){
   ios::sync_with_stdio(0);
}
long long n,a[2000005],s1[2000005],s2[2000005];
vector<int> v;
int main(){
	IO();cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i],s1[i]=n+1,s2[i]=0;
	v.push_back(1);
	for(int i=2;i<=n;i++){
		while(!v.empty()&&a[i]>a[(v.back())]){
			s1[(v.back())]=i;v.pop_back();
		}
		v.push_back(i);
	}
	v.clear();v.push_back(n);
	for(int i=n-1;i>=1;i--){
		while(!v.empty()&&a[i]>=a[(v.back())]){
			s2[(v.back())]=i;v.pop_back();
		}
		v.push_back(i);
	}
	long long ans=0;
	for(int i=1;i<=n;i++){
		long long l=s2[i]+1,r=s1[i]-1;
		ans=(ans+1ll*a[i]%mod*(i-l+1)%mod*1ll*(r-i+1)%mod)%mod;
	}
	cout<<(ans+mod)%mod<<endl;
	return 0;
}

3、逆序对距离之和

定义逆序对 \([ a[i],a[j] ]\) 的距离为 \(|i-j|\) ,求一个数组 \(a\) 里的所有逆序对距离之和。

法一:暴力

如果使用冒泡排序求解,复杂度为 \(O(n^2)\)

法二:正解

考虑传统归并排序做法,在归并的过程中求解问题。

\(i\) 开始遍历 \(left\) ,计算所有的距离差,并加到总距离中,即:

\[dis += left[i] - right[j] + left[i + 1] - right[j] + ... + left[len - 1] - right[j] \]

把右边一大坨简化一下,得出:

\[dis+=\sum_{k=i}^{len-1}{left[k]}-right[j]*(len-i) \]

在归并刚开始对着left做前缀和即可,之后每次发现逆序对,只需要O(1)的时间即可计算出所有逆序对间的距离的和。

贴代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[100005],b[100005],pos[100005];
long long cnt=0,s[100005];
void merge(int l,int r) {
    if(l>=r)return;
    int mid=(l+r)>>1,i=l,j=mid+1,k=l;
    merge(l,mid);merge(mid+1,r);
    s[l-1]=0;
    for(int i=l;i<=mid;i++)s[i]=s[i-1]+pos[a[i]];
    while(i<=mid&&j<=r) {
        if(a[i]<=a[j])b[k++]=a[i++];
        else cnt+=(long long)(mid-i+1)*pos[a[j]]-(s[mid]-s[i-1]),b[k++]=a[j++];
    }
    while(i<=mid)b[k++]=a[i++];
    while(j<=r)b[k++]=a[j++];
    for(int i=l;i<=r;i++)a[i]=b[i];
}
int main() {
    ios::sync_with_stdio(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],pos[a[i]]=i;
    merge(1,n);
    cout<<cnt<<endl;
    return 0;
}

4、

给定两个数组 \(a,b\) ,定义 \(calc(i,k)\) 操作为如果 \(k>a_i\) ,则 \(k+=b_i\)

\(q\) 次询问,每次给定 \(l,r,k\) , 求执行完

\[calc(l,k), calc(l+1,k), calc(l+2,k), \ldots, calc(r,k) \]

之后 \(k\) 的值。

\(n,q\leq 3\times 10^5\)时限3s

法一:暴力

按照题意模拟即可,时间复杂度 \(O(qn)\)

法二:优化暴力

ST表的成名之战

我们发现,如果在暴力执行的过程中, \(k\) 已经大于整个区间的最大值了,那就没必要计算了,直接加上后面所有 \(b_i\) 的和即可,可以用前缀和做到 \(O(1)\)

时间复杂度仍为 \(O(qn)\) ,但是因为出题人根本没想到有那么阴间的做法,所以甚至可以AC。

贴个代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n,q,tp,last,a[300005],b[300005],xuanxue,f[300005][20];
ll lg2[300005],pow2[300005];
int main() {
	cin>>n>>q>>tp;
	for(int i(1);i<=n;++i)cin>>a[i];
	for(int i(1);i<=n;++i)cin>>b[i],b[i]+=b[i-1];
	pow2[0]=1;lg2[0]=-1;
	for(int i(1);i<=19;++i)pow2[i]=pow2[i-1]<<1;
	for(int i(1);i<=n;++i)lg2[i]=lg2[(i>>1)]+1;
	for(int i(1);i<=n;++i)f[i][0]=a[i];
	for(int i(1);i<=19;++i){
		for(int j=1;j+pow2[i]-1<=n;++j)
			f[j][i]=max(f[j][i-1],f[j+pow2[i-1]][i-1]);
	}
	for(register int i(1);i<=q;++i){
		int l,r;ll k;cin>>l>>r>>k;
		ll logl=lg2[r-l+1],xuanxue=max(f[l][logl],f[r-pow2[logl]+1][logl]);
		for(int j=l;j<=r;++j){
			if(k>=xuanxue){k+=b[r]-b[j-1];break;}
			else if(k>=a[j])k+=b[j]-b[j-1];
		}
		cout<<k<<'\n';
	}
	return 0;
}

法三:正解

\(f_i(x)=x+[x\geq a_i]\times b_i\) 。注意这种函数的复合也是单增的。且这种函数的复合表现为一堆连续段,每个连续段是一个斜率为 \(1\) 的一次函数

考虑两个函数复合,也即把两个"连续段"复合在一起,经过计算我们发现若初始连续段数是 \(a,b\) ,那么复合后的连续段数不超过 \(a+b-1\)

所以可以在线段树的每个节点上维护"连续段",在询问时拆到 \(log\) 个线段树节点并在每个线段树节点上二分即可。

时间复杂度 \(O(n \log n + q \log^2 n)\)

上代码(呼呼呼呼呼太难写了):

#include<bits/stdc++.h>
using namespace std;
const int  MAXN=3e5+5,SZ=MAXN*4;
#define ull unsigned long long
const ull INF=1e18;
#define mid ((l+r)>>1)
int n,q,tp,a[MAXN],b[MAXN],ans;
vector<int> v1[SZ];
vector<ull>v2[SZ];
inline ull calc(int p,int i){return i==v1[p].size()-1?INF:v1[p][i+1]-1;}
void build(int p,int l,int r){
	if(l==r){
		if(a[l])v1[p].push_back(0),v2[p].push_back(0);
		v1[p].push_back(a[l]),v2[p].push_back(b[l]);
		return;
	}
	build(p*2,l,mid),build(p*2+1,mid+1,r);
	for(int i=0,j=0;i<v1[p*2].size();++i){
		while(calc(p*2+1,j)<v1[p*2][i]+v2[p*2][i])++j;
		int las=v1[p*2][i];
		while(j<v1[p*2+1].size()){
			v1[p].push_back(las),v2[p].push_back(v2[p*2][i]+v2[p*2+1][j]);
			if(calc(p*2+1,j)>=calc(p*2,i)+v2[p*2][i])break;
			las=calc(p*2+1,j)+1-v2[p*2][i],++j;
		}
	}
}
ull K;
void ask(int p,int l,int r,int L,int R){
	if(r<L||l>R)return;
	if(L<=l&&r<=R){
		int lb=0,rb=v1[p].size()-1,mid2,pos=0;
		while(lb<=rb){
			mid2=(lb+rb)>>1;
			if(v1[p][mid2]<=K)lb=mid2+1,pos=mid2;
			else rb=mid2-1;
		}
		K+=v2[p][pos];return;
	}
	ask(p*2,l,mid,L,R);
    ask(p*2+1,mid+1,r,L,R);
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;++i)scanf("%d",&a[i]);
	for(int i=1;i<=n;++i)scanf("%d",&b[i]);
	build(1,1,n);
	while(q--){
		int l,r;scanf("%d%d%llu",&l,&r,&K);
		ask(1,1,n,l,r);
		printf("%llu\n",K);
	}
	return 0;
}

5、

\(T\) 组数据,每组数据给你一个数 \(n\) , 求满足

\[1\leq b \leq a \leq n , \gcd(a,b)==a \oplus b \]

\((a,b)\) 二元组数量。 \(T\leq 3\times 10^5, n\leq 10^6\)

法一:暴力

暴力枚举 \(a,b\) ,暴力判断,复杂度 \(O(n^2 \log V)\) , 其中 \(\log V\) 是gcd复杂度, 显然无法通过。

法二:打表

如果你不着急,也可以考虑打表。

法三:正解

不妨设 \(a\geq b\)

引理 1: \(a \oplus b \geq a-b\)

证明显然......

引理 2: \(\gcd(a,b) \leq a-b\)

证明:

\[\gcd(a,b)=\gcd(b,a-b) \]

\(\gcd\) 的定义:

\[\gcd(b,a-b) \leq a-b \]

证毕。

回到原题,设 \(\gcd(a,b)=d \leq a-b\) , 则 \(d=a \oplus b\)

由引理1, \(d \geq a - b\)

\[\therefore{d = a-b} \]

\(\gcd(a,b)=a-b\) 当且仅当 $ b \mid a$ 。

所以我们枚举 \(b\)\(b\) 的倍数 \(a\) , 预处理出所有 \(n\) 的答案即可。

时间复杂度 \(O(n \log n + T)\)

#include<bits/stdc++.h>
using namespace std;
const int N=30000005;	
inline void open(const char *s){
    string str=s;
    freopen((str+".in").c_str(),"r",stdin);
    freopen((str+".out").c_str(),"w",stdout);
}
inline void close(){
    fclose(stdin);fclose(stdout);
}
inline void IO(){
    ios::sync_with_stdio(0);
}
int n,a[N];
int main(){
    IO();
    int k=N/2,T,id=0;
    for(register int i=1;i<=k;++i){
        for(int j=i*2;j<=N;j+=i){
            if((i^j)==j-i)++a[j];
        }
    }
    for(int i=1;i<=N;++i)a[i]+=a[i-1];
    cin>>T;
    while(T--){
        cin>>n;
        cout<<"Case "<<++id<<": "<<a[n]<<endl;
    }
    return 0;
}

6、

定义 \(calc(x,y)=(\phi(\gcd(x,y))==\gcd(x,y)-1)\) , 求 \(\sum_{i=1}^{n}\sum_{j=1}^{n}calc(x,y)\) 的值, \(n\leq 10^7\)

法一:暴力

暴力枚举 \(i,j\),时间复杂度 \(O(n^2 \log V)\), 其中 \(\log V\) 是gcd复杂度, 显然无法通过

法二:正解

我们考虑一个质数 \(p\)\(p\) 对答案产生的贡献,就是在区间 \([1,\lfloor\frac{n}{p_i}\rfloor]\) 中满足 \(\gcd(x,y)==1\) 的有序二元组个数。

这个贡献就是欧拉函数......

对于这道题,可以预处理欧拉函数,将得到的答案*2即可。

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=10000000;
int phi[N+75],s[N+75],p[N+75],m,n,v[N+75];
void primes(int n){
	for(int i=2;i<=n;i++) {
		if(!v[i]){v[i]=i,p[++m]=i,phi[i]=i-1;}
		for(int j=1;j<=m;j++){
			if(p[j]>v[i]||p[j]>n/i)break;
			v[i*p[j]]=p[j];
			phi[i*p[j]]=phi[i]*(i%p[j]?p[j]-1:p[j]);
		}
	}
	s[1]=0;
	for(int i=2;i<=n;++i)s[i]=s[i-1]+phi[i]*2;
}
inline void IO(){
   ios::sync_with_stdio(0);
}
int main(){
	cin>>n;primes(N);
	ll ans=0;
	for(int i=1;i<=m&&p[i]<=n;++i)ans+=s[n/p[i]]+1;
	cout<<ans<<endl;
	return 0;
}

7、

Treeland国有N个城市,从0至N-1标号。城市之间有N-1条无向的路,每条路连接一对城市。这些路构成了一棵树。(这意味着,每对城市之间可以通过一系列道路相互到达。)
  现在,我们用一个字符串数组 \(linked\) 来表示这棵树。\(linked\) 中一共有N个长度为N的字符串。 \(linked[i][j]\) 为'Y'当且仅当城市i与城市j之间有路,其他情况下,均为'N'。
  Treeland国的居民希望制造一个Treeland定位系统(TPS)。TPS将帮助人们判断自己在哪个城市。这个系统将包含K个标记信标。每个信标将安装在一个城市中。当一个人打开自己的TPS接收器时,它将会计算当前位置到各个信标的距离。(距离为最短路的长度。)
  显然,只有当每个城市对应着不同的信标读数时,TPS才可以使用。换言之,K和信标放置的位置必须使得任意两个城市收到的数字序列不同。(注意,不同的信标可以辨别出来。)
  请你计算最小的K的大小。

写在最前面

信标显然只会放在叶子节点上,因为如果放在非叶子节点上,显然会造成重复,不如放在叶子节点上方便。

法一:暴力

利用这个性质,暴力枚举所有的放置情况,一一判断即可,时间复杂度 \(O(2^{leaf(n)})\) ,其中 \(leaf(n)\) 是这个树中叶子节点个数。

法二:暴力优化

本题答案显然具有单调性。考虑二分答案,每次 \(check\) 只需要找到任意一组可行解,大大加快了速度。

可以使用 \(floyd\) 预处理最短路。

法三:卡时

考虑对法一加上卡时优化 \(\ldots\)

法四:人类智慧!

根据数学直觉!如果我们在法二二分的过程中找了许许多多组解还是没找到,那就很有可能!是无解的。

利用这个直觉,我们引入更多随机因素。

考虑用随机选取信标安放处,我的方法是随机抽取,也可以 \(randomshuffle\) 之后取前 \(mid\) 项。每次随机之后暴力判断即可。

因为出题人过于毒瘤,可能会卡我们的随机,我们认为枚举10000次是一个很安全的范围。

然后我们就在几乎不烧脑的情况下切掉了这道题。

其实这个方法比暴力还好写

代码如下

#include<bits/stdc++.h>
using namespace std;
vector<int> v[65];
int n,vis[55],one[105],tot,ch[51],g[51][51];
vector<int> d[65];
bool check2(int mid) {
	for(int i=1;i<=n;++i)d[i].clear();
	for(int i=1;i<=n;++i){
		for(int j=1;j<=mid;++j){
			d[i].push_back(g[i][ch[j]]);
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=i+1;j<=n;++j){
			int s=0;
			for(int k=1;k<=mid;++k)
				s+=d[i][k-1]==d[j][k-1];
			if(s==mid)return 0;
		}
	}
	return 1;
}
bool check(int mid) {
	for(int i=1;i<=10000;++i) {
		memset(vis,0,sizeof vis);
		for(int j=1;j<=mid;++j) {
			int t=rand()%tot+1;
			while(vis[t])t=rand()%tot+1;
			ch[j]=one[t];vis[t]=1;
		}
		if(check2(mid))return 1;
	}
	return 0;
}
int main() {
	freopen("TPS.in","r",stdin);
	freopen("TPS.out","w",stdout);
	srand(10007);
	cin>>n;
	if(n==1){
		cout<<"0\n";return 0;
	}
	memset(g,0x3f,sizeof g);
	for(int i=1;i<=n;++i) {
		g[i][i]=0;
		for(int j=1;j<=n;++j) {
			char c;cin>>c;
			if(c=='Y'){
				v[i].push_back(j);
				g[i][j]=1;
			}
		}
	}
	for(int k=1;k<=n;++k)
		for(int i=1;i<=n;++i)
			for(int j=1;j<=n;++j)
				g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
	for(int i=1;i<=n;++i)
		if(v[i].size()==1)one[++tot]=i;
	int l=1,r=tot;
	while(l<r) {
		int mid=(l+r)>>1;
		if(check(mid))r=mid;
		else l=mid+1;
	}
	cout<<l<<'\n';
	return 0;
}
法五:正解

其实不想贴的,但是毕竟不是所有人都喜欢乱搞,我还是贴上吧。

我们先假定选择了一个点为根,并且在根节点放置了信标。
设一个节点有 \(c\) 个儿子,发现必须在其中至少 \(c−1\) 个儿子的子树中放置信标。证明如下:

如果不这样放,考虑对于两棵都没有放的子树,他们汇集到 \(lca\) 上以后距离都是相等的,所以 \(lca\) 外的信标无法区分,而内部没有信标。所以不能存在两颗子树都不放,至少要放 \(c-1\) 个;

由于在根节点放置了信标,可以只考虑深度相同的点。因为他们的 \(lca\) 度数至少为 2,那么一定有一个信标在 \(lca\) 包含这两个点的两支子树中。那么另一侧的点肯定要走更远的路,会被区分开。所以放 \(c−1\) 个足够区分。

所以,对于一个确定的根,我们可以贪心地放置信标,得到当前的答案。

我们可以找到任意一个度数 \(\geq 2\) 的节点作为根,实际上这个点一定不需要放置信标,以这个点作根 \(O(n)\) 的贪心即可。

时间复杂度 \(O(n)\)

(借鉴了https://zepto.page/topcoder-srm598-tps/)

8、

定义 \(a\) 数组中 \([l,r]\) 区间的众数是 \([l,r]\) 区间内 \([l,r]\)\(a[l],a[l+1],...,a[r]\) 中出现次数最多的数,如果有若干个数出现次数相同,则众数是数较小的那个。

再定义 \(f(l,r)\)\(a\) 数组 \([l,r]\) 内的众数。

\(\sum_{i=1}^{n}\sum_{j=i}^{n}{f(l,r)}\)的值。\(n\leq 3000,-10^9\leq a_i\leq 10^9\)

法一:暴力

枚举每一个区间,暴力计算区间众数。时间复杂度 \(O(n^3 \log n)\)\(O(n^3)\),取决于算法实现。

法二:优化暴力

考虑把序列分成 \(T\) 块。

对于每个询问 \([l,r]\),设 \(l\) 处于第 \(p\) 块,\(r\) 处于第 \(q\) 块。我们把区间分成三部分考虑:

·开头不足一整段的。
·中间的整段。
·结尾不足一整段的。

显然序列在 \([l,r]\) 的众数只可能来自

posted @ 2022-11-29 13:33  JZX102624  阅读(76)  评论(1)    收藏  举报