简单
A,B,F,G,C
中等
E,I,K,L
B. 中位数
分析:我们知道不管怎么删数,只有最大值和最小值不会被删,最后剩下的只会是这两个数。
实现代码:
#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 N=1e6+2;
int n,a[N];
void slove(){
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
if(n==1)
{
cout<<a[1]<<endl;
return ;
}
sort(a+1,a+1+n);
cout<<(a[1]+a[n])/2;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
F. 中位数+4
分析:
思路分析:
这道题要求计算十进制数 n
在 k
进制表示下的后置零个数。核心思路是:反复用 k
整除 n
,直到无法整除为止,统计整除的次数即为后置零的数量。例如,n=8
,k=2
时,8 能被 2 整除 3 次(8→4→2→1),对应二进制 1000
的 3 个后置零。注意特例:若 k=10
,则直接统计十进制中末尾的 0 的数量。
实现代码:
#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 N=101;
void slove(){
int n, k;
cin >> n >> k;
int ans = 0; // 零计数器
// 循环计算n在k进制下的后置零个数
while(n % k == 0) // 当n能被k整除时
{
ans++; // 计数器加1
n /= k; // n除以k,相当于去掉一个后置零
}
cout << ans; // 输出后置零的个数
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
G. 简单题
分析:我们可以f[1]=1,f[2]=1,f[3]=2,f[4]=3,f[4]=5……,我们应该敏锐的想到斐波那契,然后得F[1]=1,F[2]=2,F[3]=4,F[4]=7,我们可以知道每三个为一组,并且每一组的第一个数对2取模就是1,其他都是0。
实现代码:
#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 N=101;
void slove(){
int n;
cin>>n;
//f[1]=1,f[2]=1,f[3]=2,f[4]=3,f[5]=5,f[6]=8
// F[1]=1,F[2]=2,F[3]=4,F[4]=7,F[5]=12,F[6]=20
if(n%3==1)
cout<<1<<endl;
else
cout<<0<<endl;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
C. 中位数+1
分析:
就是对顶堆模板。核心思想:
数据分割与平衡维护:
使用两个堆将数据流划分为两个子集:
最大堆(大顶堆):存储较小的一半元素(堆顶为最大值)
最小堆(小顶堆):存储较大的一半元素(堆顶为最小值)
核心平衡原则:0 ≤ 最大堆大小 - 最小堆大小 ≤ 1
将两个堆的“瓶口”对在一起,整体就组成了一个有序的数据结构【对顶堆】
因为我们希望快速获取【中位数】,那么插入元素时就要维护两个堆的大小大致相等,这样取数的时候就相当于在中间取。
我们始终保持大顶堆里面的元素个数大于等于小顶堆里面的元素个数,多的个数不能超过1,如果大顶堆里面的多的元素个数大于了1,为了维护平衡我们需要把小顶堆堆顶的元素,移动到大顶堆的堆顶;
如果小顶堆里面的元素个数大于大顶堆里面的元素个数,此时将小顶堆堆顶移到最大堆堆顶。
实现代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int,int>
#define fi first
#define se second
const int N=1e5+2;
void slove(){
int n;
cin>>n;
// 定义两个优先队列维护中位数:
// l: 大根堆,存储较小的一半数,堆顶是最大值
// r: 小根堆,存储较大的一半数,堆顶是最小值
priority_queue<int>l;
priority_queue<int,vector<int>,greater<int> >r;
vector<int>a; // 存储每次的中位数结果
for(int i=1;i<=n;i++){
int x;
cin>>x;
// 将新元素插入到合适的堆中
if(l.size()==0||x<l.top()){
l.push(x); // 小于左边堆顶,放入左边
}
else{
r.push(x); // 否则放入右边
}
// 平衡两个堆的大小,保持左边堆大小最多比右边大1
if(l.size()>r.size()+1){
r.push(l.top()); // 左边太大,移动一个到右边
l.pop();
}
if(l.size()<r.size()){
l.push(r.top()); // 右边太大,移动一个到左边
r.pop();
}
// 计算当前中位数
if(l.size()==r.size()){
a.push_back((l.top()+r.top())/2); // 偶数个元素,取平均值
}
else{
a.push_back(l.top()); // 奇数个元素,取左边堆顶
}
}
// 输出所有中位数结果
for(auto i:a){
cout<<i<<' ';
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
E. 中位数+3
分析:
基于数论知识,k进制下n!的后置零个数等于各质因数在n!中出现次数除以其在k中指数的最小值。
思路:
- 将k分解质因数,得到所有质因数及其指数
- 对每个质因数d,计算它在n!中出现的总次数s
- 计算方法:s = floor(n/d) + floor(n/d^2) + floor(n/d^3) + …
- 对于每个质因数d,计算s/v(v是d在k中的指数)
- 所有s/v的最小值就是n!在k进制下的后置零个数
实现代码:
#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 N=101;
const int inf=0x3f3f3f3f;
void slove(){
int n,k;
cin>>n>>k;
//分解k的质因数
vector<pii>p;//存储质因数及其指数
for(int i=2;i<=k/i;i++)//试除法分解质因数
{
if(k%i==0)
{
int c=0;//计算当前质因数的指数
while(k%i==0)
{
k/=i;
c++;
}
p.push_back({i,c});//存储质因数及其指数
}
}
if(k>1)p.push_back({k,1});//处理剩余质因数
int an=inf;
//计算每个质因数在n!中出现的次数
for(auto i:p)
{
int d=i.fi,v=i.se;//d是质因数,v是其在k中的指数
int s=0;//计算d在n!中的总出现次数
int x=n;
while(x)
{
s+=x/d;//累计当前层的贡献
x/=d;//进入下一层
}
an=min(an,s/v);//取所有质因数中的最小值
}
cout<<an<<endl;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
I. Re:从零开始的近世代数复习(easy)
分析:
主要就是倍增思想和LCA。
预处理阶段(DFS):
-
使用倍增法预处理每个节点的2^i级祖先(f数组)
-
同步预处理到各祖先的累计复习时间(g数组)
查询阶段:
初始化答案为根节点时间(必须复习)
计算两定理到LCA的路径时间(通过倍增法跳跃计算)
当LCA不是根节点时,补充计算LCA到根的路径时间。
倍增思想和LCA在这位博主的博客里有详细介绍:初识倍增思想-CSDN博客
实现代码:
#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 dep[N],fa[N]; // dep记录深度,fa记录父节点
int f[N][25]; // 倍增法求LCA的数组
int g[N][25]; // 从节点到2^i祖先的路径总复习时间
int an=0; // 记录最终答案
// DFS预处理,建立倍增数组
void dfs(int u,int fa){
dep[u]=dep[fa]+1; // 计算当前节点深度
f[u][0]=fa; // 直接父节点
// 预处理倍增数组
for(int i=1;i<=20;i++){
f[u][i]=f[f[u][i-1]][i-1]; // 2^i祖先
g[u][i]=g[f[u][i-1]][i-1]+g[u][i-1]; // 到2^i祖先的总时间
}
// 递归处理子节点
for(auto v:e[u])
dfs(v,u);
}
// 求LCA并计算路径总时间
int lca(int x,int y){
// 确保x是较深的节点
if(dep[x]<dep[y]) swap(x,y);
// x向上跳到与y同一深度
for(int i=20;i>=0;i--)
if(dep[f[x][i]]>=dep[y])
an+=g[x][i],x=f[x][i];
// 如果已经找到LCA
if(x==y){
return y;
}
// x和y同时向上跳,直到找到LCA
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i])
an+=(g[x][i]+g[y][i]),x=f[x][i],y=f[y][i];
// 加上最后一步的时间
an+=(g[x][0]+g[y][0]);
return f[x][0];
}
void slove(){
int n;cin>>n;
// 输入每个定理的复习时间,存储在g[i][0]中
for(int i=1;i<=n;i++){
cin>>g[i][0];
}
// 构建树结构
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
e[u].push_back(v);
}
// 预处理倍增数组
dfs(1,0);
int q;cin>>q;
while(q--){
int k;cin>>k;
int x,y;cin>>x>>y;
an=g[1][0]; // 初始化答案为根节点的时间
int s=lca(x,y); // 计算x和y的LCA
// 如果LCA不是根节点,还需要计算LCA到根节点的路径
if(s!=1)
lca(s,1);
cout<<an<<endl;
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
K. 狂飙追击
分析:
逆向思维的核心思想:
正向思考(从起点到目标点)可能比较复杂,因为每次移动的选择有两种(x 或 y 方向),且 m
会动态变化。
逆向思考(从目标点倒推回起点)可以更高效地计算步数:
- 如果当前
tx > ty
,说明上一步可能是从(tx - m, ty)
移动过来的。 - 如果当前
ty > tx
,说明上一步可能是从(tx, ty - m)
移动过来的
为什么 m = max(tx - ty, ty)
?
- 当
tx > ty
时:- 如果
tx
远大于ty
(tx > 2 * ty
),逆向移动的步长m
应该较大(取tx - ty
)。 - 否则,取
m = ty
(保守移动)。
- 如果
- 这样能保证逆向移动后
tx
至少减少m
,且不会跳过合法位置。
示例验证:
示例 1:(sx, sy) = (1, 1)
,(tx, ty) = (3, 2)
- 初始
(3, 2)
,tx > ty
:m = max(3 - 2, 2) = 2
。k = (3 - 1) / 2 = 1
。- 逆向移动:
tx = 3 - 1 * 2 = 1
,an = 1
。
- 现在
(1, 2)
,ty > tx
:m = max(2 - 1, 1) = 1
。k = (2 - 1) / 1 = 1
。- 逆向移动:
ty = 2 - 1 * 1 = 1
,an = 2
。
- 到达
(1, 1)
,输出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;
void slove(){
// 输入起点(sx,sy)和目标点(tx,ty)
int sx,sy,tx,ty;
cin>>sx>>sy>>tx>>ty;
int an=0; // 记录移动次数
// 逆向思维:从目标点倒推回起点
while((tx>sx||ty>sy)&&tx!=ty){ // 当未到达起点且tx≠ty时循环
if(tx>ty){ // 如果x坐标更大
int m=max(tx-ty,ty); // 计算最大街道编号
int k=(tx-sx)/m; // 计算最多可以减去的步数
if(k==0) k++; // 确保至少移动一步
tx-=k*m; // 逆向移动k步
an+=k; // 累加移动次数
}
else{ // 如果y坐标更大
int m=max(ty-tx,tx); // 计算最大街道编号
int k=(ty-sy)/m; // 计算最多可以减去的步数
if(k==0) k++; // 确保至少移动一步
ty-=k*m; // 逆向移动k步
an+=k; // 累加移动次数
}
}
// 检查是否成功到达起点
if(tx==sx&&ty==sy) cout<<an<<endl; // 输出移动次数
else cout<<-1<<endl; // 无法到达输出-1
}
/*
1. 采用逆向思维:从目标点(tx,ty)倒推回起点(sx,sy)
2. 每次选择较大的坐标进行逆向操作:
• 若tx>ty,逆向操作相当于从(x+m,y)移动过来,其中m=max(x,y)
• 同理处理ty>tx的情况
3. 通过计算可以一次性减去多步(k步),加速计算过程
4. 最终检查是否到达起点,输出结果
*/
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}
L. 防k题
分析:
算法思路:
- 每回合流程:
- 战士先行动:使用3张打击(每张伤害y2)
- 然后咔咔们攻击:初始攻击力yy1,每回合+z
- 战士策略:
- 集中攻击一个咔咔直到击败它
- 计算最少需要多少咔咔:
- 使这些咔咔在被打败前能造成≥战士血量(x2)的总伤害
- 用二分法在
[1, 1e9]
范围内快速找到最小的n
,使得n
只咔咔的总伤害 ≥ 战士血量X2
。
关键步骤:
-
单只咔咔的击败回合:
-
计算击败一只咔咔需要的打击次数
p = (x1 + y2 - 1) / y2;
(向上取整了)。 -
第
i
只咔咔被击败时的回合总数T = (i * p - 1) / 3)
(因每回合3次打击)。总伤害计算:
每只咔咔的伤害是 初始
Y1
,每回合+Z
的等差数列,总伤害公式:int D = yy1 * T + z / 2 * T * (T - 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 N=101;
int x1, x2, yy1, y2, z; // x1:咔咔血量, yy1:咔咔初始攻击力, z:咔咔每回合攻击力增量
// x2:战士血量, y2:每张打击的伤害
// 检查函数:判断n只咔咔能否在被打败前击败战士
bool check(int n) {
int sum = 0; // 累计咔咔造成的总伤害
// 计算击败一只咔咔需要的打击次数(向上取整)
// 战士每回合3张打击,每张伤害y2
int p = (x1 + y2 - 1) / y2;
// 遍历每只咔咔
for (int i = 1; i <= n; i++) {
// 计算第i只咔咔被击败时的回合总数T
// 每回合3次打击,所以(i*p-1)/3
int T = (i * p - 1) / 3;
// 计算第i只咔咔在被击败前造成的总伤害
// 等差数列求和:初始yy1,每回合+z,共T回合
int D = yy1 * T + z / 2 * T * (T - 1);
sum += D; // 累加伤害
// 如果累计伤害已经足够击败战士,提前返回
if (sum >= x2) return true;
}
return sum >= x2;
}
void slove(){
cin >> x1 >> yy1 >> z >> x2 >> y2;
// 二分查找范围初始化
int lo = 1, hi = 1e9; // 最少1只,最多1e9只咔咔
int ans; // 存储最终结果
// 二分查找最小满足条件的n
while (lo <= hi) {
int mid = lo + hi >> 1; // 计算中间值
if (check(mid)) { // 如果mid只咔咔可以击败战士
ans = mid; // 更新答案
hi = mid - 1; // 尝试更小的数量
} else {
lo = mid + 1; // 需要更多咔咔
}
}
cout << ans << endl;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
//cin>>_;
while(_--)
slove();
return 0;
}