河南萌新联赛2025第(二)场:河南农业大学

我看到花儿在绽放 我听到鸟儿在歌唱 我看到人们匆匆忙忙
我看到云朵在天上 我听到小河在流淌 我看到人们漫步在路上

河南萌新联赛2025第(二)场:河南农业大学

河南萌新联赛2025第(二)场:河南农业大学_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ

第二场,暴露了很多问题,这次的题目数学数论题居多,有点样例都看不明白,做的时候都快崩溃了。数学不好太难了;数论还没咋看,导致一些简单的题推不出来;有一些模板题没见过很不好想,但是想明白后不算难;

预估难度:

  • 简单:K I M D
  • 中等:E C B A L
  • 较难:H G
  • 困难:J F

我的做题顺序 K I M D A,数学太难了XD;虽然最后榜又歪了但是这次影响不大,自己做题的顺序也是按难度来的;


K-打瓦

签到题,使出PHP大法;

gugugaga

PHP 是一种创建动态交互性站点的强有力的服务器端脚本语言。意思就是说php语言会把不是代码的东西直接输出;


I-猜数游戏(easy)

签到题;最优策略肯定是二分的思路去猜,所以需要log2(n)次,向上取整;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
void slove(){
	int n;cin>>n;
	int an=(int)log2(n);
	if(log2(n)>an) an++;
	cout<<an;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

M-米娅逃离断头台

数学题;设大圆半径为 a a a小圆半径为 b b b从圆心连向切线要求的面积为 π × ( a 2 − b 2 ) / 2 \pi\times(a^2-b^2)/2 π×(a2b2)/2,又勾股定理可得 a 2 − b 2 = x 2 / 4 a^2-b^2=x^2/4 a2b2=x2/4,固答案为 π × x 2 / 8 \pi\times x^2/8 π×x2/8

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
const double pi=3.1415926535;
void slove(){
	int x;cin>>x;
	double c=x/2.0;
	double an=0.5*pi*c*c;
	printf("%.2lf",an);
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

D-开罗尔网络的备用连接方案

一道很简单的遍历树的题,先按要求建树,然后dfs遍历统计到达每个节点是当前值的二进制数里一的个数,最后多次寻问含有相应个数的1的节点有几个,对应查询就好;

输入边的时候顺序不是固定的,所以建树的时候记得建双向边!这样才能把图连起来;便利的时候打标记或记录父节点,防止走回头路;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
vector<int> e[N];
int a[N];
int an[N];
void dfs(int x, int w, int f){  
    int c = a[x]&w;
    bitset<35> b(c);
    an[b.count()]++;
    for (int i : e[x]) {
        if (i==f) continue; 
        dfs(i,c,x); 
    }
}
void slove() {
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= n; i++) 
        cin >> a[i];
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u); 
    }
    dfs(1,-1,0);
    while (q--) {
        int x;
        cin >> x;
        cout << an[x] << endl;
    }
}
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

这题的题目描述真是一言难尽,连最基本的需求都表达不清楚;第一次用比赛的小喇叭,没想到出题人还真的回复了;后面补充过后明白题意后操作起来就很简单;


E-咕咕嘎嘎!!!(easy)

数据很小,而且输入的是一个数,一开始想的是dfs枚举,但估计会超时,于是就想动态规划或者数学推导;但是太菜了没推出来;

题解中给出的动态规划的写法,有点类似于暴力,三重循环,i表示枚举新选进去的数,j枚举选了这个数后所有gcd的情况,k表示已经选取的序列的长度有点类似01背包,看看是加上这一位更优还是不加更优,这题里面不牵扯物品的价值价值,所以直接加也可以;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=5e3+5;
const int M=1e9+7;
int dp[N][N];
void slove(){
	int n,m;cin>>n>>m;
    for(int i=2;i<=n;i++)
        dp[i][1]=1;
	for(int i=2;i<=n;i++){ // 枚举的是选进去的数
		for(int j=2;j<i;j++){ // 枚举的是前面状态的gcd是多少
			for(int k=m;k>=2;k--){ // 枚举级精选取得序列长度的长度,每一维都相当于是个01背包,所以倒序
				dp[__gcd(i,j)][k]=max(dp[__gcd(i,j)][k],dp[__gcd(i,j)][k]+dp[j][k-1]);
				dp[__gcd(i,j)][k]%=M;
			}
		}
	}
	int an=0;
	for(int i=2;i<=n;i++)
	an+=dp[i][m],an%=M;
	cout<<an;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

C-O神

这题感觉还是比较有意思的,不太好想,被题目中设立的情境给迷惑了;比赛的时候以为就是一个大模拟,但是赛后看题解才看出来居然是个背包dp;使用 dp[j]表示消耗 j能获得的最大收益。把最优的充值情况记录下来;然后看每一抽在最优的情况下需要花费多少的钱记录在新的数组b[]中;

但是这个样例看着太奇怪了,比赛的时候根本没看懂,正常的概率不应该是小数吗?后面发现是乘法逆元的缘故:

在普通数学中,概率 p q \frac pq qp是一个小于等于1的分数。但在模数下,分数 p q \frac pq qp被表示为 p × q − 1 p\times q^{-1} p×q1 mod M M M,其中 q − 1 q^{-1} q1 q q q 的模逆元。模逆元 q − 1 q^{-1} q1通常是一个很大的数(因为 q q q是小整数时, q − 1 m o d    M q^{-1}\mod M q1modM接近 M M M)。期望计算中,每一项 b [ i ] × p × q k b[i]\times p\times q^k b[i]×p×qk在模数下会被放大 p = p q p= \frac pq p=qp在模数下是 p × q − 1 m o d    M p\times q^-1\mod M p×q1modM(大数)。 q k q^k qk ( 1 − p ) k m o d    M (1-p)^k\mod M (1p)kmodM,但通常仍是大数。最终累加时,这些大数相乘再取模,结果可能仍然很大。进一步了解好像是大二会学到的离散;

对概率逆元操作完之后,把每一抽的期望[总概率(成功的概率×当前失败的概率)×代价]计算出来累加就好了!

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=2e3+3;
const int M=1e9+7;
int dp[N],b[N];
pii a[N];
int kksu(int a,int b){
	a%=M;
	int s=1;
	while(b){
		if(b&1){
			s*=a;
			s%=M;
		}
		a*=a;
		a%=M;
		b>>=1;
	}
	return s;
}
void slove(){
	int x,p,q,m;
	cin>>x>>p>>q>>m;
	int n;cin>>n;
	for(int i=1;i<=n;i++)
	cin>>a[i].fi>>a[i].se;
	for(int i=1;i<=n;i++)
	for(int j=N;j>=a[i].fi;j--)
	dp[j]=max(dp[j],dp[j-a[i].fi]+a[i].se);
	int t=0;
	for(int i=1;i<=m;i++){
		while(dp[t]<x*i)
		t++;
		b[i]=t;
	}
	int p1=p*kksu(q,M-2)%M;
	int q1=((1-p1)%M+M)%M;
	t=1;
	int an=0;
	for(int i=1;i<m;i++){
		an+=b[i]*p1%M*t%M;
		an%=M;
		t*=q1;
		t%=M;
	}
	an+=t*b[m];
	cout<<an%M;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

但是代码里面的细节超级多,超级容易写错,因为逆元的缘故,所以基本上计算的地方都要一直去取模;


B-异或期望的秘密

这题没看懂的原因和上一题一样,不明白样例里面为什么突然变得那么大,还是对乘法逆元的理解不够深刻!

看懂后其实不难理解;

再具体的看这题之前先看下面这两题;


小蓝的二进制询问

D-小蓝的二进制询问_河南萌新联赛2024第(一)场:河南农业大学

不难发现,这道题也是这个人出的

题目要求计算区间 [l, r]内所有整数的二进制表示中1的个数之和的问题。核心策略是利用按位计算的方法高效地计算从0到任意整数n(包括n)的二进制1的个数之和,然后通过前缀和思想计算区间和。

整体的思想就是类似于前缀和的思想对于查询 [l, r],区间和等于 f(r) - f(l-1),即:
ans = ( ∑ i = 0 r popcount ( i ) ) − ( ∑ i = 0 l − 1 popcount ( i ) ) \text{ans} = (\sum_{i=0}^{r} \text{popcount}(i)) - (\sum_{i=0}^{l-1} \text{popcount}(i)) ans=(i=0rpopcount(i))(i=0l1popcount(i))

f(n)计算从0到n(包括0和n)的所有整数的二进制表示中1的个数之和。

循环条件:变量 t从2开始,每次左移一位(即t * =2),直到t>=n * 2。

周期贡献:对于每个位位置 (相当于以 t为周期),完整周期的个数是n/t。每个周期中该位为1的个数是 t/2。因此,完整周期贡献为:(n/t)*(t/2)。

以i=2,第二位为例

十进制数  二进制   第2位
0        000      0  ← 周期开始
1        001      0
2        010      0
3        011      0 
4        100      1
5        101      1
6        110      1
7        111      1
8       1000      0   ← 下一个周期开始
...

周期长度:T = 2i+1 = 8
每个周期内:前2i个数第2位为0,后4个数为1

所以得到每一位的周期就是T = 2i+1,周期内1的个数就是2i

剩余部分贡献:在一个不完整的周期中,如果余数n%t大于 t/2,则剩余部分中该位为1的个数是 n%t- t/2。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
const int M=998244353;
int f(int n){
	if(n==0) return 0;
	int s=0,t=2;
	n++;
	while(t<n*2){
		s+=n/t*(t/2);
		if(n%t>t/2) 
		s+=n%t-t/2;
		s%=M;
		t<<=1;
	}
	return s%M;
}
void slove(){
	int l,r;cin>>l>>r;
	cout<<(f(r)-f(l-1)+M)%M<<endl; 
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	cin>>_;
	while(_--)
	slove();
	return 0;
}

累加器

F-累加器_河南萌新联赛2024第(三)场:河南大学

这题要让我们看从x到x+y这期间,对应二进制的数的每一位;可以延续上一题的思路;我们看看从0到x+y中变了多少次再减去从0到x变了多少次,就是我们的结果;

这道题我们只用找到他变化的周期就行,不要再具体看有几个1了,所以更简单点;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
const int M=998244353;
int f(int x){
	int s=0;
	int t=1;
	while(t<=x){
		s+=x/t;
		t<<=1;
	}
	return s;
}
void slove(){
	int x,y;cin>>x>>y;
	cout<<f(x+y)-f(x)<<endl;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	cin>>_;
	while(_--)
	slove();
	return 0;
}

异或期望的秘密

现在我们再回来看这道题;这题看着带这个异或的字样,其实和异或关系并不大;异或时相同的位会被赋予0,所以我们去找不同的位的个数,再去比上区间长,就是我们所求的期望;

为了方便统计,我们就一位一位的看,看看这个区间的数的这一位上有几个1几个0,再看参与运算的k这一位是1还是0,找到不一样的那个数的个数,累加的分子里面;最后计算出的结果是个分数,题目要求输出逆元的形式;和上面的推导是一样的;这里每次f函数判断一位就行,遍历每一位的for在主函数中;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
const int M=1e9+7;
int kksu(int a,int b){
	int s=1;
	while(b){
		if(b&1){
			s*=a;
			s%=M;
		}
		a*=a;
		a%=M;
		b>>=1;
	}
	return s;
}
int f(int n,int i){
	if(n==0) return 0;
	int s=0,t=1<<(i+1);
	n++;
    s+=n/t*(t/2);
    if(n%t>t/2) 
    s+=n%t-t/2;
    s%=M;
	return s%M;
}
void slove(){
	int l,r,k;cin>>l>>r>>k;
	int fz=0,fm=r-l+1;
	for(int i=0;i<=30,(1ll<<i)<=max(k,r);i++){
		int v=f(r,i)-f(l-1,i);
		if(k>>i&1) 
		v=fm-v;
		fz+=v;
		fz%=M;
	}
	int an=fz*kksu(fm,M-2)%M;
	cout<<an<<endl;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	cin>>_;
	while(_--)
	slove();
	return 0;
}

A-约数个数和

朴实无华的题目描述,朴实无华的模板;一开赛就看到好多人都过了,题目还这么简短;那大概率就是模板了;一开始是毫无头绪的,因数的分解一直不太会(后来才想明白为什么是除),比赛的时候列了超级多的例子,惊奇的发现我们要求的答案就是 ∑ i = 1 n n i \sum_{i=1}^{n}\frac{n}{i} i=1nin,当时可激动了于是就暴力提交;但是数据很大超时了;于是就把每一项都列了出来,想到了之前看到过的整除分块,但是之前没具体学,好在还记得当时的思路自己推了推;

看似很简单,但是当时写完d后就一直在推推推…;好在最后还是对了;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
void slove(){
	int n;cin>>n;
	int an=0;
	for(int l=1;l<=n;l++){
		int r=n/(n/l);
		an+=(n/l)*(r-l+1);
		l=r;
	}
	cout<<an;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

整除分块

什么是整除分块?

整除分块 (又称数论分块) 是一种用于高效计算涉及整除运算的和式的技巧,常见于数论和算法问题中。其核心思想是将求和式中相同的整除结果进行“分块”,合并计算,从而将时间复杂度从 O ( n ) O(n) O(n)优化至 O ( n ) O(\sqrt{n}) O(n )

给定一个正整数 n n n ,如何快速计算以下和式?

S = ∑ k = 1 n ⌊ n k ⌋ S=\sum_{k=1}^n\lfloor\frac{n}{k}\rfloor S=k=1nkn

我们以16为例,现将结果列出来

i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 16 i 20 10 6 5 4 3 2 2 2 2 1 1 1 1 1 1 \begin{array}{|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|} \hline i & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 & 13 & 14 & 15 & 16 \\ \hline \frac{16}{i} & 20 & 10 & 6 & 5 & 4 & 3 & 2 & 2 & 2 & 2 & 1 & 1 & 1 & 1 & 1 & 1 \\ \hline \end{array} ii1612021036455463728292102111121131141151161

观察整除结果:对于 k ∈ [ 1 , n ] k\in[1,n] k[1,n], ⌊ n k ⌋ 的取值最多只有 2 n \left\lfloor\frac nk\right\rfloor\text{的取值最多只有}{2\sqrt{n}} kn的取值最多只有2n 种(因为 k ≤ n 时有 n 种取值, k > n 时 ⌊ n k ⌋ ≤ n ) k\leq\sqrt{n}\text{时有}{\sqrt{n}}\text{种取值,}{k>\sqrt{n}}\text{时}{\left\lfloor\frac nk\right\rfloor\leq\sqrt{n}}) kn 时有n 种取值,k>n knn )

合并相同值:找到所有满足 ⌊ n k ⌋ = q 的连续区间  [ l , r ] \left\lfloor\frac nk\right\rfloor=q\text{的连续区间 }{[l,r]} kn=q的连续区间 [l,r],计算该区间的贡献为 q ⋅ ( r − l + 1 ) q\cdot(r-l+1) q(rl+1)

确定右端点:对于当前分块的左端点 l ,其右端点为 r = ∣ n q ∣ l\text{,其右端点为}r=\left|\frac nq\right| l,其右端点为r= qn ,其中 q = ⌊ n l ⌋ q=\left\lfloor\frac nl\right\rfloor q=ln

看不懂?没关系来数形结合,画出 16 i \frac{16}{i} i16的函数图像,就是我们熟悉的反比例函数的图像;因为计算机的除法会向下取整,所以我们画出向下取整的函数,不难发现也是一段一段的!
与我们发现的规律一致;
函数图像

所以整除分块就是找到这些连续的块的左右端点,这样求和的时候就不用每个数都遍历一遍了;

我们可以这么理解,因为是向下取整,所以顺序遍历新得到的结果的数可能是除不尽的(可能会有余数),那我们就反除这个结果,得到恰好能除完的位置,那个位置就是我们的右端点;再往后就是新的结果了;

分块过程还是以16为例

分块区间 [l, r]q = ⌊ 16 l ⌋ \left\lfloor\frac{16}{l}\right\rfloor l16右端点 r = ⌊ 16 q ⌋ r = \left\lfloor\frac{16}{q}\right\rfloor r=q16贡献 q ⋅ ( r − l + 1 ) q \cdot (r - l + 1) q(rl+1)
[1, 1]16116 × 1 = 16
[2, 2]828 × 1 = 8
[3, 3]535 × 1 = 5
[4, 4]444 × 1 = 4
[5, 5]353 × 1 = 3
[6, 8]28(因为 ⌊ 16 2 ⌋ = 8 \left\lfloor\frac{16}{2}\right\rfloor = 8 216=82 × 3 = 6
[9, 16]1161 × 8 = 8

所以就能得到模板

for(int l=1;l<=n;l++){
    int r=n/(n/l);
    an+=(n/l)*(r-l+1);
    l=r;
}

同类练习

K-取模_2022年中国高校计算机大赛-团队程序设计天梯赛(GPLT)上海理工大学校内选拔赛

也是一道数学题;题目要求 ∑ i = 1 n ( n % i ) \sum_{i=1}^{n}\left(n\%i\right) i=1n(n%i)的结果;

我们可以得出 n % i = n − ⌊ n i ⌋ ⋅ i n\%i=n-\lfloor\frac{n}{i}\rfloor\cdot i n%i=nini

我们的问题就可以转行为 ∑ i = 1 n ( n % i ) = ∑ i = 1 n ( n − ⌊ n i ⌋ ⋅ i ) \sum_{i=1}^{n}(n\%i)=\sum_{i=1}^{n}(n-\lfloor\frac{n}{i}\rfloor\cdot i) i=1n(n%i)=i=1n(nini)也就是 = n 2 − ∑ i = 1 n ( ⌊ n i ⌋ ⋅ i ) =n^{2}-\sum_{i=1}^{n}(\lfloor\frac{n}{i}\rfloor\cdot i) =n2i=1n(⌊ini)

只看右边进而得到 ∑ x = l r x ⋅ ⌊ n i ⌋ \sum_{x=l}^{r}x\cdot\lfloor\frac{n}{i}\rfloor x=lrxin所以我们要求的就是整除分块后每一块的区间和(用等差数列求和)×这一块的值;最后累加起来用n2减;就可以了;

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int inf=0x3f3f3f3f;
const int N=1e5+5;
const int M=998244353;
void slove(){
	int n;cin>>n;
	__int128 s=0;
	for(__int128 l=1,r=1;l<=n;l++){
		int k=n/l;
		r=n/k;
		s+=((l+r)*(r-l+1)/2*k+M)%M;
        l=r;
	}
    int an=((__int128)n*(__int128)n-s)%M;
	cout<<an;
} 
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

L 题也是数论的题,推出结论就能快速解决,可结论成了大问题

H 看题解说是个数位dp,好像也能做?

G 题树形dp,比赛的时候看出来了,和之前写的没有上司的舞会比较像,但是写的时候有bug一直改不对;


这次补题的收获还是很大的,也学到了很多新的东西;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值