1012 The Best Rank(超级恶心的容器题 或者 特别好的二分题)(含二分详解)

本题传送门

解题思路

本题两种思路,一个是容器实现,另一个其实也是容器实现,但是在查找的过程中,时间复杂度从O(N)优化为了O(logN) 。并且在这里我会总结我对二分的理解。

我们得先想到这道题可能用到的容器,为了方便查询,要么其id能作为数组下标(得开一个很大的数组那种) , 要么就用map映射来实现O(1)查找。

然后为了方便统计成绩,这里要么使用多个数组,要么使用结构体,这里采用的是邻接表(essence:通过一个id去访问其所有数据),并且是unordered<string,vector> mp;

其实刚开始我也想到用这些,但是我用了很多个独立的数组,导致后面逻辑很混乱。

ac✅️代码 , 算法一:

#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <cmath>
using namespace std;

unordered_map<string, vector<int>> mp;  // 学生--成绩
vector<int> grades[4];   // 四门课程所有成绩
char names[] = "ACME";

int get_rank(const vector<int> &v, int x)//下面我们定义了cmp比较,传入的数组是降序的,所以第一个等于要找的数的下标就是排名(前面含并列)
{
    for(int i = 0; i < v.size(); i++)
    {
        if(v[i] == x)
            return i + 1;
    }
    return -1;
}

bool cmp(int a, int b)
{
    return a > b;
}

int main()
{
    int n, m;
    cin >> n >> m;
    // 输入
    for(int i = 0; i < n; i++)
    {
        string id;
        int temp[4] = {0}; // ~~初始化
        cin >> id;
        for(int i = 1; i <= 3; i++)
        {
            cin >> temp[i];//输入C,M,E
            temp[0] += temp[i];//temp[0]装的是总分数,用于求平均值
        }
        temp[0] = round(temp[0]/3.0);  // 四舍五入取成绩,总分哪怕不同,其四舍五入后的成绩也可能相等,所以不能靠总分来判断。

        for(int i = 0; i < 4; i++)
        {
            mp[id].push_back(temp[i]);  // 学生的四门成绩映射,mp[i]对应的是邻接表,把分数存进去
            grades[i].push_back(temp[i]);  // 每一门课程的所有人的分数,用于统计排名
        }
    }

    // 排序
    for(int i = 0; i < 4; i++)
        sort(grades[i].begin(), grades[i].end(), cmp);

    // 查询
    while(m--)
    {
        string id;
        cin >> id;
        if(mp.find(id) == mp.end())//mp本身可以起到查询key值是否存在的功能!
        {
            cout << "N/A" << endl;
        }
        else
        {
            // 找最高排名
            int ans = n + 1; // 这里要取四门课程中排名最小的(最佳排名)
            char c;
            for(int i = 0; i < 4; i++)  // 四门课程
            {
                int r = get_rank(grades[i], mp[id][i]);//遍历查找
                if(r < ans)
                {
                    ans = r;
                    c = names[i];//要更新科目
                }
            }
            cout << ans << " " << c << endl;
        }
    }
    return 0;

}

二分优化 重点

我们这里并没有自定义排序规则cmp

int get_rank(const vector<int> &v, int x)
{
    int l = 0, r = v.size() - 1;
    while(l < r)
    {
        int mid = (l + r + 1) >> 1;
        if(v[mid] <= x) l = mid;
        else r = mid - 1;
    }
    return v.size() - l;//这里减去的是l不是1!
}

这里的成绩数组排序是按sort默认顺序排的,从小到大,这样我们要找的最后一个等于目标的数的下标,就是排名。这样我们回到对二分的模板判断:

(至于如何判断一道题是二分题,我也不懂)

  • 这里我们要找的是一个区间的边界,对于这类问题,我们先要明确找的是哪个区间的哪个边界,先说结论:这里我们要找的是右区间靠左边界。我们这里说的区间是被“二分区间”截断的右区间,即我们这里要找的是最后一个等于目标数的位置,二分的本质就是给定一个性质x,令左边区间满足x,右边区间不满足x,即该性质将整个区间截断了。这样右边区间的靠左边界就是我们要求的最后一个等于目标数的位置。

  • 那如果我这里数组还是像上面代码一样是降序的呢,这个时候我们就要找左边区间的靠右边界,即第一个等于目标数的位置,模板就要变了。

显然后者更符合直觉

//这里得先写个cmp让数组从大到小排序
int get_rank(const vector<int>& v, int x)
{
    int l = 0, r = v.size() - 1;
    while (l < r) {
        int mid = (l + r) >> 1; 
        if (v[mid] <= x) r = mid;
        else l = mid + 1;
    }
    return l + 1;
}

二分总结:

如果是左边靠右边界:

  • 更新方式为r = mid , 在 val[mid] >= x 时成立
  • else l = mid + 1
  • mid = (l+r) / 2 , mid不加1

注意:这里我会用“靠右边界” 来记忆 “r = mid” ,但是你要是记不住,也能推导出来的,我这里只是为了更好的统一记忆,如果是大于等于 ,说明

mid取大了,要缩小r,r可能可以取到mid , 所以 r = mid , 反之可以推导出\(l\)的更新方式。

如果是右边靠左边界:

  • 更新方式为 l = mid , 在 val[mid] <= x 时成立
  • else r = mid - 1;
  • mid = (l + r + 1) / 2 , mid要加一。

如果出现 l = mid 的更新方式,mid一定要加1处理,加1处理 其实 是向上取整。如果一个区间最后只有2个数,且mid还是向下取整,当我们执行l = mid ,

我们先要知道二分的过程是不断收紧区间的过程,而这个时候l = mid 向下取整还是原来的数, 没有实现收紧区间的功能,并且接下来会陷入死循环。如果

恰好出现执行了r = mid - 1 , 这时候循环正常退出,然而,这时会WA,因为最后r 在 l左边, 含义为:要找的不存在。但是一般不要这么想,因为测试数据

有很多一定会显示TLE的情况,规范写出二分模板才是我们要做的。

注意:二分的循环条件是while(l < r),含义是当这个区间有2个及以上的数,就一直二分,当循环停止时,意味着l与r重合了。

!!!不要写成while(l<=r)了,这里两者分别有其适用的空间(等我以后再总结)

posted @ 2026-04-03 11:53  shuiwangrenjia  阅读(9)  评论(0)    收藏  举报