我看到花儿在绽放 我听到鸟儿在歌唱 我看到人们匆匆忙忙
我看到云朵在天上 我听到小河在流淌 我看到人们漫步在路上
河南萌新联赛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;虽然最后榜又歪了但是这次影响不大,自己做题的顺序也是按难度来的;
目录
- 河南萌新联赛2025第(二)场:河南农业大学
- K-打瓦
- I-猜数游戏(easy)
- M-米娅逃离断头台
- D-开罗尔网络的备用连接方案
- E-咕咕嘎嘎!!!(easy)
- C-O神
- B-异或期望的秘密
- 小蓝的二进制询问
- 累加器
- 异或期望的秘密
- A-约数个数和
- 整除分块
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-米娅逃离断头台
数学题;设大圆半径为aaa小圆半径为bbb从圆心连向切线要求的面积为π×(a2−b2)/2\pi\times(a^2-b^2)/2π×(a2−b2)/2,又勾股定理可得a2−b2=x2/4a^2-b^2=x^2/4a2−b2=x2/4,固答案为π×x2/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[]
中;
但是这个样例看着太奇怪了,比赛的时候根本没看懂,正常的概率不应该是小数吗?后面发现是乘法逆元的缘故:
在普通数学中,概率 pq\frac pqqp是一个小于等于1的分数。但在模数下,分数pq\frac pqqp被表示为 p×q−1p\times q^{-1}p×q−1 mod MMM,其中 q−1q^{-1}q−1是 qqq 的模逆元。模逆元q−1q^{-1}q−1通常是一个很大的数(因为qqq是小整数时,q−1modMq^{-1}\mod Mq−1modM接近MMM)。期望计算中,每一项b[i]×p×qkb[i]\times p\times q^kb[i]×p×qk在模数下会被放大p=pqp= \frac pqp=qp在模数下是p×q−1modMp\times q^-1\mod Mp×q−1modM(大数)。qkq^kqk是(1−p)kmodM(1-p)^k\mod M(1−p)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=0rpopcount(i))−(∑i=0l−1popcount(i))\text{ans} = (\sum_{i=0}^{r} \text{popcount}(i)) - (\sum_{i=0}^{l-1} \text{popcount}(i))ans=(i=0∑rpopcount(i))−(i=0∑l−1popcount(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=1nni\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)。
给定一个正整数nnn ,如何快速计算以下和式?
S=∑k=1n⌊nk⌋S=\sum_{k=1}^n\lfloor\frac{n}{k}\rfloor S=k=1∑n⌊kn⌋
我们以16为例,现将结果列出来
i1234567891011121314151616i201065432222111111\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],⌊nk⌋的取值最多只有2n\left\lfloor\frac nk\right\rfloor\text{的取值最多只有}{2\sqrt{n}}⌊kn⌋的取值最多只有2n种(因为 k≤n时有n种取值,k>n时⌊nk⌋≤n)k\leq\sqrt{n}\text{时有}{\sqrt{n}}\text{种取值,}{k>\sqrt{n}}\text{时}{\left\lfloor\frac nk\right\rfloor\leq\sqrt{n}})k≤n时有n种取值,k>n时⌊kn⌋≤n) 。
合并相同值:找到所有满足⌊nk⌋=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⋅(r−l+1)。
确定右端点:对于当前分块的左端点l,其右端点为r=∣nq∣l\text{,其右端点为}r=\left|\frac nq\right|l,其右端点为r=qn,其中q=⌊nl⌋q=\left\lfloor\frac nl\right\rfloorq=⌊ln⌋。
看不懂?没关系来数形结合,画出16i\frac{16}{i}i16的函数图像,就是我们熟悉的反比例函数的图像;因为计算机的除法会向下取整,所以我们画出向下取整的函数,不难发现也是一段一段的!
与我们发现的规律一致;
所以整除分块就是找到这些连续的块的左右端点,这样求和的时候就不用每个数都遍历一遍了;
我们可以这么理解,因为是向下取整,所以顺序遍历新得到的结果的数可能是除不尽的(可能会有余数),那我们就反除这个结果,得到恰好能除完的位置,那个位置就是我们的右端点;再往后就是新的结果了;
分块过程还是以16为例
分块区间 [l, r] | q = ⌊16l⌋\left\lfloor\frac{16}{l}\right\rfloor ⌊l16⌋ | 右端点 r=⌊16q⌋r = \left\lfloor\frac{16}{q}\right\rfloor r=⌊q16⌋ | 贡献 q⋅(r−l+1)q \cdot (r - l + 1) q⋅(r−l+1) |
---|---|---|---|
[1, 1] | 16 | 1 | 16 × 1 = 16 |
[2, 2] | 8 | 2 | 8 × 1 = 8 |
[3, 3] | 5 | 3 | 5 × 1 = 5 |
[4, 4] | 4 | 4 | 4 × 1 = 4 |
[5, 5] | 3 | 5 | 3 × 1 = 3 |
[6, 8] | 2 | 8(因为 ⌊162⌋=8\left\lfloor\frac{16}{2}\right\rfloor = 8 ⌊216⌋=8) | 2 × 3 = 6 |
[9, 16] | 1 | 16 | 1 × 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=1n(n%i)\sum_{i=1}^{n}\left(n\%i\right)∑i=1n(n%i)的结果;
我们可以得出n%i=n−⌊ni⌋⋅in\%i=n-\lfloor\frac{n}{i}\rfloor\cdot in%i=n−⌊in⌋⋅i
我们的问题就可以转行为∑i=1n(n%i)=∑i=1n(n−⌊ni⌋⋅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(n−⌊in⌋⋅i)也就是=n2−∑i=1n(⌊ni⌋⋅i)=n^{2}-\sum_{i=1}^{n}(\lfloor\frac{n}{i}\rfloor\cdot i)=n2−∑i=1n(⌊in⌋⋅i)
只看右边进而得到∑x=lrx⋅⌊ni⌋\sum_{x=l}^{r}x\cdot\lfloor\frac{n}{i}\rfloor∑x=lrx⋅⌊in⌋所以我们要求的就是整除分块后每一块的区间和(用等差数列求和)×这一块的值;最后累加起来用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一直改不对;
这次补题的收获还是很大的,也学到了很多新的东西;