Leetcode 77 数组中的最大数对和 | 统计坏数对的数目

1 题目

2815. 数组中的最大数对和

给你一个下标从 0 开始的整数数组 nums 。请你从 nums 中找出和 最大 的一对数,且这两个数数位上最大的数字相等。

返回最大和,如果不存在满足题意的数字对,返回 -1

示例 1:

复制代码
输入:nums = [51,71,17,24,42]
输出:88
解释:
i = 1 和 j = 2 ,nums[i] 和 nums[j] 数位上最大的数字相等,且这一对的总和 71 + 17 = 88 。 
i = 3 和 j = 4 ,nums[i] 和 nums[j] 数位上最大的数字相等,且这一对的总和 24 + 42 = 66 。
可以证明不存在其他数对满足数位上最大的数字相等,所以答案是 88 。

示例 2:

复制代码
输入:nums = [1,2,3,4]
输出:-1
解释:不存在数对满足数位上最大的数字相等。

提示:

  • 2 <= nums.length <= 100
  • 1 <= nums[i] <= 104

2 代码实现

cpp 复制代码
class Solution {
public:
int maxSum(vector<int>& nums) {
    pair<int, int> digitPairs[10] = {{-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1},
                                     {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}};
    for (int num : nums) {
        int maxDigit = 0;
        int temp = num;
        while (temp > 0) {
            maxDigit = max(maxDigit, temp % 10); 
            temp /= 10; 
        }
        int secondMax = digitPairs[maxDigit].first; 
        int firstMax = digitPairs[maxDigit].second; 

        if (num > firstMax) {
            secondMax = firstMax;
            firstMax = num;
        } else if (num > secondMax) {
            secondMax = num;
        }

        digitPairs[maxDigit] = {secondMax, firstMax};
    }

    int result = -1;
    for (int i = 0; i < 10; ++i) {
        int secondMax = digitPairs[i].first;
        int firstMax = digitPairs[i].second;
        if (secondMax != -1 && firstMax != -1) {
            result = max(result, secondMax + firstMax);
        }
    }

    return result;
}
};

见鬼了easy题不会用一次遍历,说白了练的太少了,我还是生疏又生疏。

题解

第一步:先搞懂 "数位最大数字" 怎么算(最基础的部分)

比如数字 71,咱们要找它每一位里最大的数:

  • 71 的最后一位是 1(71%10=1),最大数暂时是 1;
  • 去掉最后一位变成 7(71/10=7),最后一位是 7(7%10=7),比 1 大,最大数更新成 7;
  • 再去掉最后一位变成 0,循环结束。所以 71 的 "数位最大数字" 是 7。

这个操作很固定,不管什么数字,都用 "取余(%10)+ 整除(/10)" 就能算出来,记死这个套路就行。

第二步:为什么要 "按数位分类,存最大的两个数"

题目要找 "数位最大数字相等" 的两个数,而且和最大。那咱们可以这么想:

  • 把所有数字按 "数位最大数字" 分成 10 类(0~9,比如 71、17 都归到 "7" 类);
  • 每一类里,只要有两个数,它们的和就是这个类里的 "候选最大和";
  • 要让这个类的和最大,只需要留这个类里最大的两个数就行(比如 "7" 类里有 71 和 17,留这两个,它们的和就是最大的)。

所以咱们不用管其他数,每一类只记前两名(最大、次大),效率最高。

第三步:用代码一步一步跟着做

核心代码实现

cpp 复制代码
// 核心函数:解决数组中的最大数对和问题
int maxSum(vector<int>& nums) {
    // 定义pair数组,下标:数位的最大值(0~9),pair<次大值, 最大值>,初始值为-1(表示无数字)
    pair<int, int> digitPairs[10] = {{-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1},
                                     {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}};

    // 第一步:一次遍历数组,更新每个数位对应的最大两个数
    for (int num : nums) {
        // 1. 计算当前数字的数位中最大的数字
        int maxDigit = 0;
        int temp = num;
        while (temp > 0) {
            maxDigit = max(maxDigit, temp % 10); // 取最后一位并更新最大数位
            temp /= 10; // 去掉最后一位
        }

        // 2. 更新当前数位对应的次大值和最大值
        int secondMax = digitPairs[maxDigit].first; // 次大值
        int firstMax = digitPairs[maxDigit].second; // 最大值

        if (num > firstMax) {
            // 新数字比最大值大:次大值变为原来的最大值,最大值变为新数字
            secondMax = firstMax;
            firstMax = num;
        } else if (num > secondMax) {
            // 新数字比次大值大、比最大值小:仅更新次大值
            secondMax = num;
        }

        // 把更新后的值存回数组
        digitPairs[maxDigit] = {secondMax, firstMax};
    }

    // 第二步:遍历数位数组,计算最大数对和
    int result = -1;
    for (int i = 0; i < 10; ++i) {
        int secondMax = digitPairs[i].first;
        int firstMax = digitPairs[i].second;
        // 只有次大值和最大值都不为-1时,才存在有效数对
        if (secondMax != -1 && firstMax != -1) {
            result = max(result, secondMax + firstMax);
        }
    }

    return result;
}
cpp 复制代码
int maxSum(vector<int>& nums) {
    // 1. 准备一个"分类容器":10个位置(0~9),每个位置存两个数(次大、最大),初始都是-1(表示没数字)
    pair<int, int> digitPairs[10] = {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1},
                                     {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}};

    // 2. 遍历每个数字,逐个分类、更新最值
    for (int num : nums) {
        // 2.1 先算当前数字的"数位最大数字"(套固定套路)
        int maxDigit = 0;
        int temp = num; // 临时变量,避免修改原数字
        while (temp > 0) {
            maxDigit = max(maxDigit, temp % 10); // 取最后一位,更新最大数位
            temp = temp / 10; // 去掉最后一位
        }

        // 2.2 把当前数字放进对应的"类"里,只留最大的两个数
        int 次大 = digitPairs[maxDigit].first;
        int 最大 = digitPairs[maxDigit].second;
        if (num > 最大) { // 比当前最大还大,挤掉原来的最大
            次大 = 最大;
            最大 = num;
        } else if (num > 次大) { // 比次大大,但比最大小,挤掉原来的次大
            次大 = num;
        }
        digitPairs[maxDigit] = {次大, 最大}; // 把更新后的前两名存回去
    }

    // 3. 找所有类里"次大+最大"的最大值
    int result = -1;
    for (int i = 0; i < 10; ++i) {
        int 次大 = digitPairs[i].first;
        int 最大 = digitPairs[i].second;
        if (次大 != -1 && 最大 != -1) { // 这个类里至少有两个数
            result = max(result, 次大 + 最大);
        }
    }
    return result;
}

现在试着代入示例 1 走一遍(彻底理解)

示例 1:nums = [51,71,17,24,42]

  1. 51 的数位最大是 5 → 归到 "5" 类,类里变成(-1,51);
  2. 71 的数位最大是 7 → 归到 "7" 类,类里变成(-1,71);
  3. 17 的数位最大是 7 → 归到 "7" 类,17 > 次大(-1)但 < 最大(71),类里变成(17,71);
  4. 24 的数位最大是 4 → 归到 "4" 类,变成(-1,24);
  5. 42 的数位最大是 4 → 归到 "4" 类,42>24,类里变成(24,42);
  6. 最后算所有类的和:"7" 类 17+71=88,"4" 类 24+42=66 → 最大是 88。

关键:不用死记复杂逻辑,记 3 个 "固定动作"

  1. 算数位最大数字:用 while 循环 +%10+%10,套公式就行;
  2. 分类存最值:每个类只留前两名,按 "比最大大→更新两个,比次大大→更新一个" 来;
  3. 算最终结果:遍历所有类,有两个数的就求和,取最大。

超详细题解

先给一个通俗的比喻(理解核心逻辑)

把这个问题想象成:

  • 学校举办运动会,有10 个项目(对应 0~9 这 10 个 "数位最大数字"),每个学生(对应数组里的数字)只能参加一个项目(比如数字 71 的数位最大是 7,就参加项目 7;数字 17 的数位最大也是 7,也参加项目 7)。
  • 现在要从每个项目里选两名学生,让他们的体重和最大(对应数字和最大),最后在所有项目的组合里,选体重和最大的那个。
  • 那我们根本不用记每个项目的所有学生,只需要记每个项目里体重最大的两个学生(最大、次大)就行,因为这两个的和肯定是该项目里最大的。

这个比喻就是代码的核心逻辑,接下来我们结合示例和代码逐行走一遍。


结合示例 1 逐行拆解代码(nums = [51,71,17,24,42])

我们先把代码里的变量名换成更直白的中文注释,再跟着示例走:

cpp 复制代码
int maxSum(vector<int>& nums) {
    // 定义:digitPairs[0~9] 对应10个项目,每个项目存<次大数字, 最大数字>,初始都是-1(表示没人参加)
    pair<int, int> digitPairs[10] = {{-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1},
                                     {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}};

    // 遍历每个数字(每个学生)
    for (int num : nums) {
        // 第一步:算当前数字属于哪个项目(数位最大数字)
        int maxDigit = 0; // 项目编号
        int temp = num; // 临时变量,比如num=71时,temp=71
        while (temp > 0) {
            maxDigit = max(maxDigit, temp % 10); // 取最后一位:71%10=1 → maxDigit=1;7%10=7 → maxDigit=7
            temp /= 10; // 去掉最后一位:71→7→0,循环结束
        }
        // 此时,num=71的maxDigit=7(属于项目7),num=17的maxDigit=7(也属于项目7)

        // 第二步:把当前数字加入对应项目,只留最大的两个
        int 次大 = digitPairs[maxDigit].first; // 项目里的次大数字
        int 最大 = digitPairs[maxDigit].second; // 项目里的最大数字

        if (num > 最大) { // 比当前最大的还大,挤掉原来的最大
            次大 = 最大; // 原来的最大变成次大
            最大 = num; // 当前数字变成新的最大
        } else if (num > 次大) { // 比次大的大,比最大的小,挤掉原来的次大
            次大 = num;
        }

        // 把更新后的结果存回去
        digitPairs[maxDigit] = {次大, 最大};
    }

    // 第三步:找所有项目里,两个数字的和的最大值
    int result = -1;
    for (int i = 0; i < 10; ++i) {
        int 次大 = digitPairs[i].first;
        int 最大 = digitPairs[i].second;
        // 只有项目里有至少两个人(次大和最大都不是-1),才计算和
        if (次大 != -1 && 最大 != -1) {
            result = max(result, 次大 + 最大);
        }
    }

    return result;
}
现在跟着示例 1 的数字逐个走一遍(关键步骤)

示例 1 的 nums = [51,71,17,24,42],我们逐个处理每个数字:

  1. 处理第一个数字:51

    • 算 maxDigit:51 的数位是 5 和 1,maxDigit=5(属于项目 5)。
    • 项目 5 当前是 {-1, -1},所以最大=51次大=-1,存回去:digitPairs [5] = {-1, 51}。
  2. 处理第二个数字:71

    • 算 maxDigit:71 的数位是 7 和 1,maxDigit=7(属于项目 7)。
    • 项目 7 当前是 {-1, -1},所以最大=71次大=-1,存回去:digitPairs [7] = {-1, 71}。
  3. 处理第三个数字:17

    • 算 maxDigit:17 的数位是 1 和 7,maxDigit=7(属于项目 7)。
    • 项目 7 当前是 {-1, 71},17 > 次大(-1),所以次大=17最大=71,存回去:digitPairs [7] = {17, 71}。
  4. 处理第四个数字:24

    • 算 maxDigit:24 的数位是 2 和 4,maxDigit=4(属于项目 4)。
    • 项目 4 当前是 {-1, -1},所以最大=24次大=-1,存回去:digitPairs [4] = {-1, 24}。
  5. 处理第五个数字:42

    • 算 maxDigit:42 的数位是 4 和 2,maxDigit=4(属于项目 4)。
    • 项目 4 当前是 {-1, 24},42 > 最大(24),所以次大=24最大=42,存回去:digitPairs [4] = {24, 42}。
最后计算所有项目的和
  • 项目 5:只有 51(次大 =-1),不计算。
  • 项目 7:17+71=88,当前 result=88。
  • 项目 4:24+42=66,比 88 小,result 还是 88。
  • 其他项目都是 - 1,不计算。
  • 最终返回 88,和示例结果一致。

再解释你可能卡壳的两个点

1. 为什么只需要存最大的两个数?

比如项目 7 里有 71、17,哪怕还有其他数(比如 7),71+17 的和也肯定比 71+7、17+7 的和大。所以只要保留最大的两个数,它们的和就是该类别下的最大值,不用存所有数,这是代码高效的关键。

2. 为什么初始值是 - 1?

因为题目里的数字都是正整数(1≤nums [i]≤10^4),用 - 1 表示 "该类别下还没有数字",这样后续判断 "是否有至少两个数" 时,只要看次大和最大是不是都不等于 - 1 就行。


总结

  1. 核心逻辑:按 "数位最大数字" 分类,每个类别只保留最大的两个数,最后算这些数对的和的最大值
  2. 代码三步曲:
    • 算每个数字的 "数位最大数字"(确定类别);
    • 把数字加入对应类别,更新该类别的最大、次大值;
    • 遍历所有类别,计算有效数对的和的最大值。
  3. 记住那个 "运动会项目" 的比喻,就能把抽象的代码逻辑转化成直观的场景,就不会懵了。

3 题目

2364. 统计坏数对的数目

给你一个下标从0 开始的整数数组 nums 。如果 i < jj - i != nums[j] - nums[i] ,那么我们称 (i, j) 是一个 数对

请你返回 nums坏数对 的总数目。

示例 1:

复制代码
输入:nums = [4,1,3,3]
输出:5
解释:数对 (0, 1) 是坏数对,因为 1 - 0 != 1 - 4 。
数对 (0, 2) 是坏数对,因为 2 - 0 != 3 - 4, 2 != -1 。
数对 (0, 3) 是坏数对,因为 3 - 0 != 3 - 4, 3 != -1 。
数对 (1, 2) 是坏数对,因为 2 - 1 != 3 - 1, 1 != 2 。
数对 (2, 3) 是坏数对,因为 3 - 2 != 3 - 3, 1 != 0 。
总共有 5 个坏数对,所以我们返回 5 。

示例 2:

复制代码
输入:nums = [1,2,3,4,5]
输出:0
解释:没有坏数对。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109

4 代码实现

cpp 复制代码
class Solution {
public:
    long long countBadPairs(vector<int>& nums) {
        int n = nums.size();
        long long total = (long long )n* (n - 1) / 2 ;
        unordered_map <int , long long > keyCount ;
        long long goodPairs = 0 ;

        for (int i = 0 ; i < n  ; i++){
            int key = nums[i] - i ;
            goodPairs += keyCount[key] ;
            keyCount[key]++;
        }
        return total - goodPairs;
    }
};

超时了错误的代码

绝对超时了,是不是用unordered_map,不知怎么写哇...

cpp 复制代码
class Solution {
public:
    long long countBadPairs(vector<int>& nums) {
        int n = nums.size();
        int count = 0 ;
        for (int i = 0 ; i < n ; i ++){
            for (int j = n - 1 ; j > i ; j --){
                if (j - i != nums[j]- nums[i]){
                    count ++;
                }
            }
        }
        return count ;
    }
};

你现在要解决的是 "统计坏数对的数目" 问题,你的嵌套循环写法确实会超时(时间复杂度(O(n^2)),n 大的时候根本跑不通),而核心优化思路就是用unordered_map来统计规律,把时间复杂度降到(O(n))。我先帮你理清思路,再给出完整代码。

核心需求梳理

你需要统计数组中满足j - i ≠ nums[j] - nums[i] 的数对 (i, j)(i < j)的数量,直接嵌套循环会超时,需要用哈希表(unordered_map)找数学规律来优化。

解题思路(关键是数学变形 + 反向统计)

首先做数学公式变形,这是解题的核心:

完整代码实现

cpp 复制代码
#include <vector>
#include <unordered_map>
using namespace std;

class Solution {
public:
    long long countBadPairs(vector<int>& nums) {
        int n = nums.size();
        // 1. 计算所有数对的总数(组合数:n选2)
        long long total = (long long)n * (n - 1) / 2;
        // 2. 用unordered_map统计每个key(nums[k]-k)出现的次数
        unordered_map<int, long long> keyCount;
        long long goodPairs = 0; // 好数对数量

        for (int k = 0; k < n; k++) {
            int key = nums[k] - k;
            // 注意:这里要先累加当前key已出现的次数,再更新map
            // 因为当前k和之前出现的所有同key的下标i(i < k)都能组成好数对
            goodPairs += keyCount[key];
            keyCount[key]++;
        }

        // 3. 坏数对 = 总数 - 好数对
        return total - goodPairs;
    }
};

代码关键部分解释

  1. 总数计算 :用(long long)n * (n - 1) / 2是为了避免整数溢出(n 可能很大,比如 1e5,n*(n-1) 会超过 int 的范围,所以先转 long long)。
  2. key 的定义key = nums[k] - k,这是从数学变形中得到的核心标识。
  3. 好数对统计逻辑
    • 遍历每个下标 k 时,keyCount[key]表示在 k 之前 ,有多少个下标 i 的 key 和当前 k 的 key 相同。这些 i 都能和 k 组成好数对(因为 i < k),所以先把这个数量加到goodPairs里。
    • 然后把当前 key 的计数加 1,供后续下标使用。
  4. 最终结果:总数减去好数对数量,就是坏数对数量。

举个例子帮你理解(比如 nums = [4,1,3,3])

  • n=4,总数 = 4*3/2=6。
  • 遍历每个 k:
    • k=0,key=4-0=4,keyCount[4]=0 → goodPairs=0 → keyCount[4]=1。
    • k=1,key=1-1=0,keyCount[0]=0 → goodPairs=0 → keyCount[0]=1。
    • k=2,key=3-2=1,keyCount[1]=0 → goodPairs=0 → keyCount[1]=1。
    • k=3,key=3-3=0,keyCount [0]=1 → goodPairs +=1(现在 goodPairs=1)→ keyCount [0]=2。
  • 好数对数量 = 1,坏数对数量 = 6-1=5。
goodPairs += keyCount[key] ;
keyCount[key]++; //这两行什么意思?

这两行是统计好数对 的核心逻辑,也是最容易绕晕的地方,我用通俗的语言 + 具体例子给你讲透,保证你能懂。

先明确两个前提

  1. 好数对的条件 :下标i < k(注意这里把原来的j换成了k,只是变量名变了),且nums[i]-i = nums[k]-k(也就是两个下标对应的key相等)。
  2. keyCount[key]的含义 :在遍历到当前下标k时,keyCount[key]存储的是k之前(也就是i < k),有多少个下标ikey和当前kkey相等

逐行解释

第一行:goodPairs += keyCount[key];

这句话的意思是:把当前k能和前面的i组成的好数对数量,加到总好数对里

比如,当遍历到k时,keyCount[key]是 3,说明前面有 3 个下标ikeyk的一样,那么k就能和这 3 个i分别组成 3 个好数对,所以总好数对要加 3。

第二行:keyCount[key]++;

这句话的意思是:把当前kkey加入统计,让后续的下标能找到它

因为后续会遍历到k+1k+2等下标,这些下标需要知道 "前面有多少个和自己key相同的下标",所以要把当前kkey计数加 1。


用具体例子一步一步走(最直观)

还是用之前的例子:nums = [4,1,3,3],我们逐个遍历下标k,看这两行的执行过程:

初始状态
  • goodPairs = 0
  • keyCount是空的(没有任何key的统计)
1. 遍历k=0nums[0]=4
  • key = 4 - 0 = 4
  • 此时keyCount[4] = 0(前面没有任何下标),所以goodPairs += 0goodPairs还是 0。
  • 然后keyCount[4]++keyCount[4] = 1(现在统计里有了 k=0 的 key)。
2. 遍历k=1nums[1]=1
  • key = 1 - 1 = 0
  • 此时keyCount[0] = 0,所以goodPairs += 0 → 还是 0。
  • 然后keyCount[0]++keyCount[0] = 1(统计里有了 k=1 的 key)。
3. 遍历k=2nums[2]=3
  • key = 3 - 2 = 1
  • 此时keyCount[1] = 0,所以goodPairs += 0 → 还是 0。
  • 然后keyCount[1]++keyCount[1] = 1(统计里有了 k=2 的 key)。
4. 遍历k=3nums[3]=3
  • key = 3 - 3 = 0
  • 此时keyCount[0] = 1(前面有 k=1 的 key 是 0),所以goodPairs += 1goodPairs变成 1。
  • 然后keyCount[0]++keyCount[0] = 2(统计里有了 k=1 和 k=3 的 key)。

为什么不能反过来写?(易错点提醒)

如果把两行顺序换成:

cpp 复制代码
keyCount[key]++;
goodPairs += keyCount[key];

会出问题!因为这样会把当前 k 自己 也算进去(keyCount[key]变成了 "前面的数量 + 1"),导致统计的好数对数量变多(比如上面的 k=3,会加 2 而不是 1),结果就错了。

所以必须先累加前面的数量,再把自己加入统计,这是关键顺序。

总结

  1. goodPairs += keyCount[key]:统计当前下标k能和前面的下标组成的好数对数量,加到总数里。
  2. keyCount[key]++:把当前下标kkey加入统计,供后续下标使用。
  3. 这两行的顺序不能颠倒,否则会错误地把当前下标自己算进去,导致结果出错。
  4. 核心优化点是数学公式变形 (将坏数对条件转化为好数对的nums[k]-k相等)和反向统计(总数减好数对),避免了嵌套循环。
  5. unordered_map 的作用是统计每个 key 的出现次数,从而快速计算好数对数量,时间复杂度降到 (O(n))。
  6. 注意整数溢出问题,需要用long long类型存储总数、好数对数量等变量。
相关推荐
长安er5 小时前
LeetCode 235 & 236 最近公共祖先(LCA)解题总结
算法·leetcode·二叉树·递归·lca
代码游侠5 小时前
学习笔记——Linux 进程管理笔记
linux·运维·笔记·学习·算法
lxmyzzs5 小时前
【图像算法 - 38】工业巡检应用:基于 YOLO 与 OpenCV 的高精度管道缺陷检测系统实现
opencv·算法·yolo·管道检测
老鱼说AI5 小时前
算法基础教学:哈希表
数据结构·算法·散列表
解局易否结局5 小时前
GitCode口袋工具开发学习
学习·gitcode
逐辰十七5 小时前
freertos学习笔记12--个人自用-第18章 资源管理(Resource Management)
笔记·学习
lxmyzzs5 小时前
【图像算法 - 39】环保监测应用:基于 YOLO 与 OpenCV 的高精度水面垃圾检测系统实现
opencv·算法·yolo·水下垃圾检测
点云SLAM5 小时前
Redundant 英文单词学习
人工智能·学习·英文单词学习·雅思备考·redundant·冗余的·多余的 、重复的
linsa_pursuer5 小时前
回文链表算法
java·算法·链表