个人题解(A M F L C E G)

之前单发的G好像不太够字数,于是水一下个人解法,为了照顾萌新我会尽量讲明白一点。

// 之后补题会顺便更新

A

一个比较显然的结论:1与任何数互质

考虑石头个数为偶数的情况,此时你只能拿走奇数个石头,那么轮到对方时一定是奇数个石头;

石头个数为奇数的话,此时我拿一个,轮到对方就是偶数个石头;

这意味着如果我先手且开局为奇数的话,轮到我的时候永远是奇数,偶数同理。

显然1是奇数,于是结论很显然了。

代码如下:

#define ll long long

void solve(){
    ll n;cin>>n;
    if(n%2==1){
        cout<<"Yes\n";
    }
    else if(n%2==0){
        cout<<"No\n";
    }
}

M

简单统计,略

F

小学解方程,略

L

神秘模拟,好奇为什么一开始没人做,一开始C过的都比这个多

上手画一下会发现无论多少层都有解,这里分享一下我自己的画法

alt

注意到沿着红色箭头走下来之后,按照以下方式操作若干次即可:向左走到头,然后上下横跳走到头。

然后观察一下数字规律,一个循环走下来即可。

注意层数的定义!

代码如下:

void solve(){
    int n;cin>>n;
    n++;//层数定义看错导致的
    cout<<"Yes\n";
    int pre=1;
    cout<<pre<<' ';
    for(int i=1;i<n;i++){
        pre=i+pre;
        cout<<pre<<' ';
    }
    int t=n;
    while(t--){
        for(int i=1;i<=t;i++){
            pre=pre+1;
            cout<<pre<<' ';
        }

        for(int i=1;i<=2*t-1;i++){
            if(i%2==1){
                pre=pre-t-1;
                cout<<pre<<' ';
                
            }
            else{
                pre=pre+t;
                cout<<pre<<' ';
            }
        }
    }
}

C

前置知识:字典树的定义,只需要了解一棵字典树长什么样子就可以了。

本题可以直接套字典树板子,当然排序也可以,简单说一下为什么字典树做法是对的:

首先注意到只有退格,那么我们只需要关注前缀就可以了。

朴素的贪心思想:所有前缀都应该利用完价值再删除,即:一个公共前缀删除前,应使带有该前缀的字符串全部出现过。这个很显然,无需证明。

那么什么顺序最优呢?其实不重要,我们只需要使得最长的字符串最后出现即可:因为除了最后一个字符串,其他的字符串我们都会删除掉。

再看字典树,我们会发现:敲键盘打字的次数实际上就是字典树中所有字符的数量——我们定义这个数量为

删除次数=打字次数 最长字符串长度

那么

代码如下(随便抄了个板子):

#include<iostream>
#include<vector>
#include<string>
using namespace std;

struct TrieNode {
    TrieNode* children[26];
    TrieNode() {
        for (int i = 0; i < 26; ++i) children[i] = nullptr;
    }
    ~TrieNode() {
        for (int i = 0; i < 26; ++i)
            if (children[i]) delete children[i];
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<string> words(n);
    for (int i = 0; i < n; ++i)
        cin >> words[i];
    
    while (m--) {
        int l, r;
        cin >> l >> r;
        l--; r--;
        vector<string> q;
        for (int i = l; i <= r; ++i)
            q.push_back(words[i]);
        
        TrieNode* root = new TrieNode();
        int sum = 0;
        int max_len = 0;
        
        for (const auto& word : q) {
            TrieNode* current = root;
            for (char ch : word) {
                int index = ch - 'a';
                if (!current->children[index]) {
                    current->children[index] = new TrieNode();
                    sum++;
                }
                current = current->children[index];
            }
            if (word.size() > max_len) max_len = word.size();
        }
        
        int result = sum * 2 - max_len;
        cout << result << '\n';
        delete root;
    }
    return 0;
}

顺带提一下排序的方法:

本质上其实是一样的,因为按照字典序排了之后相同前缀的会挨在一起,不赘述了

#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<string> words(n);
    for (int i = 0; i < n; ++i)
        cin >> words[i];
    
    while (m--) {
        int l, r;
        cin >> l >> r;
        l--; r--;
        vector<string> q;
        for (int i = l; i <= r; ++i)
            q.push_back(words[i]);
        
        sort(q.begin(), q.end());
        if (q.empty()) {
            cout << "0\n";
            continue;
        }
        
        int sum_len = 0;
        int max_len = 0;
        for (auto& s : q) {
            sum_len += s.size();
            if (s.size() > max_len) max_len = s.size();
        }
        
        int total_lcp = 0;
        for (int i = 1; i < q.size(); ++i) {
            const string& a = q[i-1];
            const string& b = q[i];
            int lcp = 0;
            while (lcp < a.size() && lcp < b.size() && a[lcp] == b[lcp])
                ++lcp;
            total_lcp += lcp;
        }
        
        int ans = (sum_len - total_lcp) * 2 - max_len;
        cout << ans << '\n';
    }
    return 0;
}

E

神秘二分,竟然一遍过了。

简单说一下思路:

如何统计碰撞次数呢?对于每个向右移动的小球,统计所有在其右侧向左移动的小球数量即是总碰撞次数。道理很简单:因为碰撞交换速度且两球速率相同,我们可以认为两个球穿透了彼此继续按照原速度移动(高中物理好像讲过,等效替代);

注意到小球速度都是一样的,那么给定时间 t,对于两个小球而言,当且仅当一个小球向右移动,另一个小球向左移动,并且它们之间的距离在 t 时间内可以被缩短到 0,它们才会发生碰撞。同样使用我们上述的等效替代考虑,任意两个球的移动区间有交集则认为这两个球发生了一次碰撞。

要球经过多长时间发生k次碰撞的话就很简单了,二分时间后check次数即可。

看了一眼精度感觉跑个几十次check就行了,保险起见跑了150次

#define ll long long

ll cal(double t, const vector<int>& sr, const vector<int>& sl) {
    int nr=sr.size(), nl=sl.size();
    ll res=0;
    double two_t=2*t;
    int l=0, r=0;
    for(int x:sr){
        double limit=x+two_t;
        while(l<nl&&sl[l]<=x)l++;
        while(r<nl&&sl[r]<=limit)r++;
        res+=r-l;
    }
    return res;
}

void solve(){
    int n,k;
    cin>>n>>k;
    vector<int> sr,sl;
    for(int i=0;i<n;++i){
        int p,v;
        cin>>p>>v;
        if(v==1)sr.push_back(p);
        else sl.push_back(p);
    }
    sort(sr.begin(),sr.end());
    sort(sl.begin(),sl.end());
    
    ll total=0;
    int l=0,nl=sl.size();
    for(int x:sr){
        while(l<nl&&sl[l]<=x)l++;
        total+=nl-l;
    }
    if(total<k){
        cout<<"No\n";
        return;
    }
    
    double low=0.0,high=0.0;
    if(!sr.empty()&&!sl.empty()&&sr[0]<sl.back()){
        high=(sl.back()-sr[0])/2.0;
    }
    
    for(int _=0;_<150;++_){
        double mid=(low+high)*0.5;
        ll cnt=cal(mid,sr,sl);
        if(cnt>=k)high=mid;
        else low=mid;
    }
    cout<<"Yes\n"<<fixed<<setprecision(7)<<high<<'\n';
}

G:

第一眼看过去就知道肯定是整除分块,对于每个块对应的余数,我们可以将其看作一段长为等差数列。

我们定义一个分块为Block:

struct Block { int a, b, q; }; // [a,b]除数区间,对应商为q

其中为除数区间,对于任意属于[a,b],有

此时我们知道,对于任意属于的值构成一个等差数列,我们知道此时区间内的最大余数为,即

不妨暴力的考虑:将所有的分块放入一个优先队列中,按照最大余数的大小进行排序,每次从区间中取出一个最大余数,更新区间,再将其放回队列中,直到取满k个元素。

显然对于k=1e9的数据规模这是必定超时的。

如何优化呢?

这里有一个很巧妙的方法:我们可以二分计算出一个阈值T,T表示大于等于T的余数至少有k个。

那么问题就迎刃而解了:我们只需要遍历所有区间,每个区间取出所有大于等于T的余数,再把多余的等于T的余数减去即可。

代码:

#define ll long long

struct Block { int a, b, q; }; // [a,b]除数区间,对应商为q
vector<Block> blocks;

void build(int n) {
    blocks.clear();
    for(int q=1, r; q<=n; q=r+1) {
        r = n/(n/q); // 计算当前商对应的除数区间右边界
        blocks.push_back({q, r, n/q}); // 存储除数区间[q,r]和商
        if(r == n) break; // 终止条件
    }
}

ll count(int T, int n) {
    ll cnt = 0;
    for(auto &blk : blocks) {
        int q_val = blk.q;
        int max_i = (n - T)/q_val; // 最大满足条件的除数
        int low = max(blk.a, 1);    // 除数区间下限
        int high = min(blk.b, max_i);
        if(high >= low) cnt += high - low + 1;
    }
    return cnt;
}

ll sum(int T, int n) {
    ll s = 0;
    for(auto &blk : blocks) {
        int q_val = blk.q;
        int max_i = (n - T)/q_val;
        int low = max(blk.a, 1);
        int high = min(blk.b, max_i);
        
        if(high >= low) {
            int cnt = high - low + 1;
            int first = n - q_val*low;  // 首项余数
            int last = n - q_val*high;  // 末项余数
            s += (ll)(first + last)*cnt/2;
        }
    }
    return s;
}

void solve() {
    int n, k;
    cin >> n >> k;
    
    build(n); // 构建分块
    
    // 二分求临界值T
    int l=0, r=n, T=0;
    while(l <= r) {
        int mid = (l+r)/2;
        count(mid, n) >= k ? (T=mid,l=mid+1) : (r=mid-1);
    }
    
    ll total = sum(T, n);
    ll cnt = count(T, n);
    cout << total - (cnt - k)*T << endl;
}
全部评论

相关推荐

2025-12-27 16:21
已编辑
门头沟学院 Java
bg:中下211本科,java后端,无竞赛,无基础,大一升大二暑假开始学java。五段实习:美团-小红书-腾讯-淘天-字节。面秋招的简历只有美团、小红书、淘天。刚刚发现我的秋招蚂蚁流程挂了,这是我最后一个流程,那么我的秋招就算彻底结束了,总结一下:字节ssp+,职级2-1。美团ssp,+2打了半小时微信电话极力挽留。快手ssp,但报了字节薪资后没有争取的想法了。小红书sp,今年小红书给的很高,但比字节2-1还是差很多。虾皮应该是小sp?对虾皮一点意向都没,纯拿来集邮了。淘天ssp(暑期转正),说不要我的三方,毕业前考虑好了随时可以不签三方选择淘天。挂了的流程:京东二面挂,估计学历被卡了。懂车帝一面挂,和面试官聊不来,不认同我的方案。拼多多hr面挂,问我低于预期还来不来,当时说不考虑了,估计觉得我不忠诚。蚂蚁hr面挂,聊的还行,但估计我不会去给我挂了吧。阿里控股一面挂,没面前就知道是kpi了,因为时间可选的很多,而且都是半小时,我也拿他刷我的kpi了。上面差不多是我的情况,下面是我想说的话。我觉得我不算特别突出优秀的那类人,但我多少也算是靠前的那一批人,即使这样,秋招也不算特别顺利,也有挂了的流程,但你能说是我的问题吗,我觉得大部分情况不是的,如果真的是我的问题,我不可能本科校招拿到2-1,所以很多面试挂了,问题不出在面试者身上,很多是看运气+眼缘+和面试官合不合得来。所以我觉得,学会察言观色,了解面试官的脾性,也是面试很重要的一个点。比如面试官是喜欢听长回答,还是听短回答,他更看重哪些点,每个面试官对这些的侧重都是不一样的,所以作为面试者,要学会察言观色,通过面试官开局的一两个问题以及你回答后他的表现,就要判断出来。像我现在其实面试开局个五分钟,我就基本能判断个七七八八了,然后我后面的回答就会有所变化。这是我想说的第一个点:不要为面试结果焦虑,有时候问题不出在你身上,但你可以学一些面试技巧,尽量提高你的面试通过率,这里说的面试技巧指的不是网上那种烂大街的,一两分钟短视频说什么提高你面试通过率的,而是你要在你自己的面试过程中不断总结经验,吸取教训,旁人教你的终究是有限的。另外想说下选offer的事,上面其实可以看出来,我秋招最后是选了字节的,还没签三方我就来提前实习感受业务了,当我签完三方又过了一个多月,我这些天又在想这个问题,字节真的是我想要的吗,我现在总结了一下字节的好坏,发现当时可能被字节的高薪资影响判断了,如果现在再选一次的话,我应该会选杭州的小红书,会生活的更舒服点。具体种种就不展开说了。然后虽然我现在也可以说去把小红书舔回来,去毁字节,但我觉得没必要这么做,我可以采用其他的措施去不就,比如规划好两年内就跳槽,跳到杭州,跳到更舒适的城市。我觉得大家选offer的时候,真的可以冷静下来多方面考虑,薪资、城市、组内氛围、业务、老板是否看重、组内情况、未来升职机会等等都是可以考虑的因素,虽然有的时候不管选哪个,都不会坏,但最好也别让自己后悔吧,即使真后悔了,我觉得也没必要过度美化没走过的路,想好补救措施即可。这是我想说的第二个点:冷静好好做选择,不管是offer还是其他。但人生容错率很大,即使选错了,也一定有补救措施。最后还想说一些成长上的东西,尤其是现在AI火热的时代。我觉得大家如果想提高自己,或者说在未来社招跳槽有竞争力,肯定是要学AI相关的东西的,不说要会多懂AI,至少也要了解基本概念,而且一定要学会用AI提效。我现在字节的mt和我说,他现在80%代码都是AI写的。而我最近也开始尝试用AI工具,感觉现在AI真的进步很多,挺聪明的了,我现在写需求基本都是先让AI写,我再人工review小改动一下就差不多了。我觉得「AI取代程序员」是个很远的话题,但是「AI取代不会用AI的程序员」,可能真的就是近两年的事了。而怎么去学习这块的内容,其实我也正在探索,我也是刚学AI的起步阶段,我觉得大家也要有自己的信息检索能力,而不是别人喂你什么,你才学什么,自己一个人就不会学了。这是我想说的第三个点:趁年轻,多学习提升自己,拥抱AI,不要原地踏步,原地踏步的程序员最容易被淘汰。大概就是这样吧,今天看蚂蚁流程发现挂了,前几天腾讯约面我也拒了,就想到自己的秋招/校招算彻底结束了,有感而发,随便聊了下。牛客以后应该不会更新,大家不用关注,熟悉我的朋友应该知道我在其他平台有号。我更喜欢以长视频的形式去做分享,感觉会更有体系,而不是网上那种一两分钟的零碎短视频的那种营销号去起号,我也推荐大家多去看高质量的长文章、长视频,我觉得收获的能更多。希望大家能收获满意的offer与未来。
CEXBB:刷到最后才发现原来是优雅✌🏻,我的Java引路人
2025年终总结
点赞 评论 收藏
分享
评论
3
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务