牛客寒假训练营第一场个人思路和解法

个人的第一篇博客,也是第一篇题解。


甚至markdown都是刚刚在隔壁官网学的

这是一篇半年前比赛的题解。

由于是第一次写这种东西,我去参考了世界第一可爱的嘤嘤嘤的题解

以下,按照这个分级给出我的个人思路


A 茕茕孑立之影

题干:一个由 n 个数字组成的数组 {a1,a2,⋯ ,an},找到一个不大于 1018 的正整数 x,满足 x 和数组中任意一个元素都互不为倍数关系,即对于 i∈[1,n],x 不是 ai​ 的倍数,且 ai 也不是 x 的倍数。

思路:要求不满足倍数关系,可以很容易想到,质数不会是除1之外任何其他数字的倍数,而足够大的数就可以保证没有ai会是它的倍数,所以先判断ai中有没有1,如果有1则显然不可能得到答案,输出-1,否则,直接用一个大质数跟小红爆了

ai范围在109,所以只要大于109的质数就行,可以打表打出,或者直接用10n+7套入isprime试出一个大质数

代码如下:

#include<bits/stdc++.h>
using namespace std;
long long N=1000000007;
int main(){
    int t;
    cin>>t;
    int n;
    while(t--){
        cin>>n;
        int p;
        long long ans=N;
        while(n--){
            cin>>p;
            if(p==1){
                ans=-1;
                break;
            }
        }
        cout<<ans<<endl;
    }
}

B 一气贯通之刃

题干:一棵树,请你寻找一条简单路径,使得这条路径不重不漏的经过所有节点。如果不存在这样的简单路径,则直接输出 -1。

思路:最简单的图论题。
想到了之前手机上的智力小游戏:一笔连线 (想当年我玩这个也是一把好手(guess启蒙),后来才知道原来有规律hhh)。

由于给定的是一棵树,树上是没有环的,所以想要一笔画出这棵树,每个节点最多只能和两个边相连(一进一出)。所以这棵树实际上是一条链。
所以遍历所有节点,如果节点的度不是两个1和n-2个2就无解,输出-1;反之有解,输出两个1节点的编号

代码:

#include<bits/stdc++.h>
using namespace std;
int a[100002];
int main(){
    int n;
    cin>>n;
    n--;
    n*=2;
    int t;
    while(n--){
        cin>>t;
        a[t]++;
        if(a[t]>2){
            cout<<-1;
            return 0;
        }
    }
    int x=-1,y=-1;
    for(int i=0;i<100002;i++){
        if(a[i]==1){
            if(x==-1)x=i;
            else if(y==-1)y=i;
            else {
                cout<<-1;
                return 0;
            }
        }
    }
    cout<<x<<" "<<y;
    return 0;
}

D 双生双宿之决

题干:小红定义一个数组是“双生数组”,当且仅当该数组大小为偶数,数组的元素种类恰好为 2 种,且这两种元素的出现次数相同。例如 {1,1,4,4,1,4}是双生数组。现在小红拿到了一个数组,她希望你判断该数组是不是双生数组。

思路:非常简单的模拟题,我的思路是先判断n如果是奇数则直接判否,否则排序数组,再判断第1个元素和第n/2个元素是否相同、第n/2+1个元素和第n个元素是否相同、第1个和第n个元素是否不同。只有以上三个判断都为真时输出yes,否则输出no。

代码

#include<bits/stdc++.h>
using namespace std;
int main(){
	int T;
	cin>>T;
	while(T--){
		int n;
		cin>>n;
		int a[n];
		for(int i=0;i<n;i++){
			cin>>a[i];
			
		}
		if(n%2==1){
			cout<<"No"<<endl;
			continue;
		}
		sort(a,a+n);
		
		if(a[0]==a[n/2-1]&&a[n/2]==a[n-1]&&a[0]!=a[n-1])cout<<"Yes"<<endl;
		else cout<<"No"<<endl;
	}
}

G 井然有序之衡

题干:小红拿到了一个数组,她可以进行任意次以下操作:选择两个元素,使得其中一个加 1,另一个减 1。
小红希望最终数组变成一个排列,请你帮助他确定这能否实现。如果可以实现的话,还需要求出最小操作次数。

长度为 n 的排列是由 1∼n 这 n 个整数、按任意顺序组成的数组,其中每个整数恰好出现一次。例如,{2,3,1,5,4} 是一个长度为 5 的排列,而 {1,2,2} 和 {1,3,4} 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

思路:乍一看似乎“能否变成排列”和“所有元素的和”有点关系,因为操作并没有改变这些元素的和,+1和-1抵消了。直观想象中,一些“很大很大”的元素应该不断-1,而“较小”的元素则不断+1。这里的大和小应该和排列比较,并且尽量让这个“不规则曲线”去拟合y=x这条表示排列的曲线。
所以想到,应该从小到大排列数组,并用一个变量累积 真实数组元素 减去 “预期”(排列元素)的差值。如果最后这个变量为零,则说明正负刚好抵消,也就是有解,否则就是无解。
操作次数的统计很简单,用一个变量只积累“顺差”,或者积累所有差值的绝对值再除以2,得到的就是操作次数。

代码:

#include<bits/stdc++.h>
 
using namespace std;
 
int main(){
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++){
        cin >> a[i];
    }
    a[0] = -2e9;
    ranges::sort(a);
    auto sum = 0ll;
    auto ans = 0ll;
    for(int i = 1; i <= n; i++){
        sum += a[i] - i;
        if(a[i] < i) ans += i - a[i];
    }
    if(sum) ans = -1;
    cout << ans << endl;
}

E 双生双宿之错

题干:小红定义一个数组是“双生数组”,当且仅当该数组大小为偶数,数组的元素种类恰好为 2 种,且这两种元素的出现次数相同。例如 {1,1,4,4,1,4} 是双生数组。
现在小红拿到了一个长度为偶数的数组,她可以进行若干次操作,每次操作将选择一个元素,使其加 1 或者减 1。小红希望你计算将该数组变成双生数组的最小操作次数。

思路:有点小guess成分的题(不guess可以写三分)。
很容易看出,把一个数组变成所谓的“双生数组”,就是把数组分成两部分,前一半全部变成一个较小的数,后一半变成一个较大的数。
所以先对数组排序,从中间分成两半,分别为p和q;然后分别确定一个x和一个y,代表p和q的“目标”。如果已经确定了x和y,则只需要累加p数组每个数和x差值的绝对值、q数组每个数和y的差值的绝对值,把他们相加就是最小操作次数。

所以,现在的问题变成了,如何选取x和y可以使差值之和最小。
首先想到平均数。平均数能代表一组数的平均水平,好像是比较“靠中间”,似乎会使得差值小一些?但很容易就想到问题:其实“比较平均”的数值对于累积差值没有什么帮助。在一个只有1和100两个数的数组中,取x=50和取x=2的效果是一样的。我发现,当取值增加1时,比新取值小的数字都多“贡献”了一点差值,比新取值大或者和新取值相等的数字都少贡献了一点差值;取值减少1时则相反。所以,想取得最优值,就要取这两个变化效果相同的那点,也就是“左边数字数量”等于“右边数字数量”时,即中位数。

代码:

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

int a[100002];
signed main(){
	int T;
	cin>>T;
	while(T--){
		int n;
		cin>>n;
		for(int i=0;i<n;i++){
			cin>>a[i];
		}
		sort(a,a+n);
		int x,y;
		if(n%4!=0){
			x=a[n/4];
			y=a[n/4+n/2];
		}else{
			x=(a[n/4-1]+a[n/4])/2;
			y=(a[n/4*3-1]+a[n/4*3])/2;
		}
		int ans=0;
		//cout<<x<<" "<<y<<endl;
		if(x==y){
			x--;
			int ansa=0;
			for(int i=0;i<n/2;i++){
				ansa+=abs(a[i]-x);
			}
			for(int i=n/2;i<n;i++){
				ansa+=abs(a[i]-y);
			}
			x++;
			y++;
			int ansb=0;
			for(int i=0;i<n/2;i++){
				ansb+=abs(a[i]-x);
			}
			for(int i=n/2;i<n;i++){
				ansb+=abs(a[i]-y);
			}
			ans=min(ansa,ansb);
		}else{
			for(int i=0;i<n/2;i++){
				ans+=abs(a[i]-x);
			}
			for(int i=n/2;i<n;i++){
				ans+=abs(a[i]-y);
			}
		}
		cout<<ans<<endl;
	}
}

H 井然有序之窗

题干:小红希望你构造一个长度为 n 的排列,需要满足第 i 个元素的范围在 [li,ri] 范围内。你能帮帮她吗?

长度为 n 的排列是由 1 ~ n 这 n 个整数、按任意顺序组成的数组,其中每个整数恰好出现一次。例如,{2,3,1,5,4} 是一个长度为 5 的排列,而 {1,2,2} 和 {1,3,4} 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

思路:首先直观想象一下,给定的是一些范围{l,r},每一个范围指定了数组中第i个数字的大小。由于每个范围有一定宽度,我们可以在这个范围内随意取。但是因为要构造的是排列,所以如果我们取值的方法不够聪明,可能会导致轮到某个位置取数时,它所有可用值都已经被之前的取值取过了。

下面解释这题中最关键的一点,这也是同类型贪心法的关键。
让我们从小到大考虑:假设现在已经轮到x取值,也就是说,比x小的数已经全部取完;也就是说,比x小的“空间”已经没用了,不会在有比x小的数字去占据这些空间;也就是说,我们不再需要关注这些空间了。
从此,我们把所有l<x的[l,r]区间都看成[x,r]区间。此时,为了给后来的数字留出尽可能多的空间,我们应该选择“浪费”空间最少的,也就是r最小的那个区间。这种思考方法也称作:

该操作【不会使答案变劣】

我们可以在很多贪心法的题解中看到这句话。在这道题中,就是“在i一定时,取r最小的区间一定不会使答案变劣”。

但大部分题解到这里就没有了,接下来就在代码部分中喊着优先队列啊、左右端点排序啊什么的就冲过来了。但是因为我很菜看不懂希望给出更全面的解释,包括一些个人的问题和具体实现上的细节。

首先,当我们看完题解自觉完全理解了,胸有成竹地打开编辑器,闭眼写完cin之后,我们就会面临第一个问题:“从小到大”是怎么操作的?

毕竟,这些东西是一些有长有短,在数轴各处卧着的一大群毛虫。要找出“覆盖了某点的所有没有用过的毛虫”然后找出它们之中右端点最小的一个,我们应该怎么下手呢?毕竟,我们可以想到,它们可能是头部枕着这一点、身体压着这一点、尾巴放在这一点上,多只毛虫也可能叠在一起,简直就是地狱绘图啊(

先不要继续想象一群毛虫爬动的样子了,让我们从0开始操作。很容易想到,覆盖了0点的毛虫很可能不止一条,但是它们肯定是头部在0点上。我们把它们都捉来,摆在案板上,比较右端点,找出最小的一个,然后,其余的毛虫留在案板上,再从数轴上捉来包含1的毛虫。此时,因为前面的毛虫(头部在1之前而身体在1上的毛虫)都被捉走了,所以它们肯定是头部在1点上。

发现了吗?我们用“把没用完的毛虫留在案板上”的方法,避免了复杂的分析。这也就是“左端点排序”的意义,我们每次把“左端点等于i”的区间拿来,就可以保证区间左端点小于等于i的区间都在“案板”上,不会遗漏。

此外,每次我们还需要把“不够长”的毛虫扔掉。毕竟,虽然右端点越小的毛虫似乎“更好”,更加“勤俭节约”,但是如果右端点比当前需要填充的值还要小,那就不行了。它们不可能为大于右端点的元素提供空间,所以没有价值了,就会被狠狠优化(悲

但是,对于这道题来说,这种优化是不允许的。因为这些区间和答案数组的索引一一对应,如果有一个区间被扔掉了,答案数组就会有一个空。这显然不符合排列的定义。所以,如果要扔区间了,就直接判否。

最后,看看实现上的问题。
对于“案板”的选择,因为我们只需要对它做两种操作:抓毛虫放到案板上、找出目前案板上右端点最小的毛虫。所以显然,可以使用优先队列,重写小于号,让它自动按照右端点从小到大排好。
而对于数组左端点的排序,可以手写比较函数,让左端点小的区间“更小”,排在前面。

代码:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define _for(l, r) for (ll i = (l); i <= (ll)(r); i++)
#define _refor(l, r) for (ll i = (l); i >= (ll)(r); i--)
const int N = 500005;
int ans[N];
struct node
{
    int l,r,i;
    bool operator<(const node&other) const{
        return r>other.r;
    }
}a[N];
bool cmp(node x,node y){
    return x.l<y.l;
}
int main() {
    int n;
    cin>>n;
    priority_queue<node> nqu;
    _for(0,n-1){
        cin>>a[i].l>>a[i].r;
        a[i].i=i;
    }
    sort(a,a+n,cmp);
    int cnt=0;
    _for(1,n){
        while(cnt<n&&a[cnt].l<=i){
            nqu.push(a[cnt]);
            cnt++;
        }
        if(nqu.empty()){
            cout<<-1<<endl;
            return 0;
        }
        node nw=nqu.top();
        nqu.pop();
        if(nw.r<i){
            cout<<-1<<endl;
            return 0;
        }else{
            ans[nw.i]=i;
        }
    }
    _for(0,n-1){
        cout<<ans[i]<<" ";
    }
}

J 硝基甲苯之袭

题干:小苯拿到了一个由 n 个整数构成的数组 {a1,a2,…,an},他想知道,从中任取两个元素 ai 和 aj (i<j),满足 ai xor⁡ aj=gcd⁡(ai,aj)的方案数有多少?

gcd⁡,即最大公约数,指两个整数共有约数中最大的一个。例如,12 和 30 的公约数有 1,2,3,6,其中最大的约数是 6,因此 gcd⁡(12,30)=6。‌

xor 表示按位异或运算。

思路:第一眼,数论题。数论没学,过(逃

直观想象一下,两个数的xor和两个数的最大公因数相等……感觉两个东西都很不直观。
要求的是方案数,所以想到枚举所有方案。由于这里有三个变量: ai 、 aj、gcd(ai,aj),所以我们需要选择两个作为自变量。
ai和aj是完全等价的,所以先选一个ai。然后,我们想要遍历次数尽量少。gcd(ai,aj)一定会是ai的因子,数量肯定比ai本身少,也就是比aj小,所以我们选这个。

然后,思路已经出来了。首先遍历全部的ai,对于每一个ai遍历它的全部因子,用这个因子和ai求出aj,判断aj是否存在,再用ai和aj判断这个因子是不是gcd。有一种先借再还的感觉。

具体实现上有很多细节。
首先,怎么存储这些数值?由于这些数字完全没有顺序的差别,所以可以使用黑科技map,将数字存储成“数值:个数”的形式,在判断通过准备累加的时候,累加 ai的个数 乘以 aj的个数 次,大大减少因为重复数值带来的重复遍历。听说有人想离散化卡了一个小时? STL好啊 STL得学啊(
如何根据ai和因子求出aj?在这里,我们使用xor的运算性质

按位异或运算自反性 : 如果 x xor y == z 则 x xor z == y

原式变为 ai xor gcd(ai,aj)== aj.可以求出。

最后,可以想到(或者看到答案输出的全都是二倍),每一个取法都算了两遍。再除以二。

代码:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define _for(l, r) for (ll i = (l); i <= (ll)(r); i++)
#define _refor(l, r) for (ll i = (l); i >= (ll)(r); i--)
const int N = 500005;
ll a[N];
int main() {
    //由按位异或运算自反性可知,ai xor aj == gcd(ai,aj)其实是求 ai xor gcd(ai,aj)== aj
    //aj通过枚举ai的因子(gcd必定是ai因子)来获得,再检查ai,aj的gcd是否是所枚举的因子即可
    //先求出顺序对+逆序对的总数,将其除以2便是答案
    int n;
    cin >> n;
    map<int, int> mp;
    for (int i = 0; i < n; ++i) {
        int x;
        cin >> x;
        mp[x]++;
    }
    ll ans = 0;
    for (auto &it : mp) {
        int x = it.first;
        // 处理所有因数i
        for (int i = 1; i * i <= x; ++i) {
            if (x % i != 0) continue;
            // 检查i对应的y  
            
            int y = x ^ i;// ai xor gcd(ai,aj)
            if (mp.count(y) && __gcd(x, y) == i) {//如果y存在 并且 因数i是xy的最大公因数
                ans += (ll)it.second * mp[y];
            }
            // 检查另一个较大的因数k = x/i
            int k = x / i;
            if (k == i) continue; // 避免重复处理
            y = x ^ k;
            if (mp.count(y) && __gcd(x, y) == k) {
                ans += (ll)it.second * mp[y];
            }
        }
    }
    cout << ans/2;
}

M 数值膨胀之美

题干:小红拿到了一个数组,她准备进行恰好一次操作:选择一个非空区间,将其中所有元素都乘以 2
小红希望最小化数组的极差,你能帮帮她吗?

数组的极差为:数组的元素最大值减去最小值。

思路:中文题也能读假。
我读成了任意次操作。自己~吓自己~。打codeforce打的。
而且,要求输出的是极差,而不是操作区间。也就是说,操作区间不需要记录。
首先,直观想象一下。显然,我们应该把小的元素乘2,这样它们就可能更加接近大的元素。但是这里有两个问题。第一,小的元素乘2之后有可能反而大大超过大元素,比如100和99;第二,小的元素并没有放在一起,如果最小值和次小值之间夹着最大值,显然我们只能扩大最小值,而且因为次小值得不到扩大,所以会成为极差的“硬伤”。
所以其实并没有想象中那么复杂。我们只需要依次找到最小值、第二小值、第三小值……并且把寻找过程中经过的点都扩大到二倍就行了。当然这个过程不是“越来越优”的,也就是说,答案会在这个过程中某步出现。所以我们要在每一个想找的点被找到之后,更新一下极差最小值。
每次的新极差都是新最大值减去新最小值,其中新最大值为原最大值或者刚刚被翻倍了的元素,新最小值为刚刚被翻倍的元素(当前最小值)或者次小值。

代码:

#include<bits/stdc++.h>
pair<int,int>a[202020];
int b[202020];
int main(){
	int n,i;
	cin>>n;
	for(i=0;i<n;i++){
		cin>>a[i].first,a[i].second=i;
		b[i]=a[i].first;
	}
	a[n].first=2e9;
	sort(a,a+n);
	int l=a[0].second,r=a[0].second;
	int ma=max(a[0].first*2,a[n-1].first);
	int res=ma-min(a[0].first*2,a[1].first);
	for(i=1;i<n;i++){
		while(a[i].second<l){
			l--;
			ma=max(ma,b[l]*2);
		}
		while(a[i].second>r){
			r++;
			ma=max(ma,b[r]*2);
		}
		res=min(res,ma-min(a[0].first*2,a[i+1].first));
	}
	cout<<res;
}
posted @ 2025-05-12 12:24  風笙咲  阅读(41)  评论(0)    收藏  举报
页面特效
正在加载今日诗词....