我看到花儿在绽放 我听到鸟儿在歌唱 我看到人们匆匆忙忙
我看到云朵在天上 我听到小河在流淌 我看到人们漫步在路上
河南萌新联赛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 π×(a2−b2)/2,又勾股定理可得 a 2 − b 2 = x 2 / 4 a^2-b^2=x^2/4 a2−b2=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×q−1 mod M M M,其中 q − 1 q^{-1} q−1是 q q q 的模逆元。模逆元 q − 1 q^{-1} q−1通常是一个很大的数(因为 q q q是小整数时, q − 1 m o d M q^{-1}\mod M q−1modM接近 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×q−1modM(大数)。 q k q^k qk是 ( 1 − p ) k m o d M (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
=
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=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;
}
累加器
这题要让我们看从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=1∑n⌊kn⌋
我们以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}}) k≤n时有n种取值,k>n时⌊kn⌋≤n) 。
合并相同值:找到所有满足 ⌊ 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⋅(r−l+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⋅(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(因为 ⌊ 16 2 ⌋ = 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 = 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=n−⌊in⌋⋅i
我们的问题就可以转行为 ∑ 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(n−⌊in⌋⋅i)也就是 = n 2 − ∑ i = 1 n ( ⌊ n i ⌋ ⋅ i ) =n^{2}-\sum_{i=1}^{n}(\lfloor\frac{n}{i}\rfloor\cdot i) =n2−∑i=1n(⌊in⌋⋅i)
只看右边进而得到 ∑ x = l r x ⋅ ⌊ n i ⌋ \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一直改不对;
这次补题的收获还是很大的,也学到了很多新的东西;