1 题目
给你一个下标从 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 <= 1001 <= 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]
- 51 的数位最大是 5 → 归到 "5" 类,类里变成(-1,51);
- 71 的数位最大是 7 → 归到 "7" 类,类里变成(-1,71);
- 17 的数位最大是 7 → 归到 "7" 类,17 > 次大(-1)但 < 最大(71),类里变成(17,71);
- 24 的数位最大是 4 → 归到 "4" 类,变成(-1,24);
- 42 的数位最大是 4 → 归到 "4" 类,42>24,类里变成(24,42);
- 最后算所有类的和:"7" 类 17+71=88,"4" 类 24+42=66 → 最大是 88。
关键:不用死记复杂逻辑,记 3 个 "固定动作"
- 算数位最大数字:用 while 循环 +%10+%10,套公式就行;
- 分类存最值:每个类只留前两名,按 "比最大大→更新两个,比次大大→更新一个" 来;
- 算最终结果:遍历所有类,有两个数的就求和,取最大。
超详细题解
先给一个通俗的比喻(理解核心逻辑)
把这个问题想象成:
- 学校举办运动会,有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],我们逐个处理每个数字:
-
处理第一个数字:51
- 算 maxDigit:51 的数位是 5 和 1,maxDigit=5(属于项目 5)。
- 项目 5 当前是 {-1, -1},所以
最大=51,次大=-1,存回去:digitPairs [5] = {-1, 51}。
-
处理第二个数字:71
- 算 maxDigit:71 的数位是 7 和 1,maxDigit=7(属于项目 7)。
- 项目 7 当前是 {-1, -1},所以
最大=71,次大=-1,存回去:digitPairs [7] = {-1, 71}。
-
处理第三个数字:17
- 算 maxDigit:17 的数位是 1 和 7,maxDigit=7(属于项目 7)。
- 项目 7 当前是 {-1, 71},
17 > 次大(-1),所以次大=17,最大=71,存回去:digitPairs [7] = {17, 71}。
-
处理第四个数字:24
- 算 maxDigit:24 的数位是 2 和 4,maxDigit=4(属于项目 4)。
- 项目 4 当前是 {-1, -1},所以
最大=24,次大=-1,存回去:digitPairs [4] = {-1, 24}。
-
处理第五个数字: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 就行。
总结
- 核心逻辑:按 "数位最大数字" 分类,每个类别只保留最大的两个数,最后算这些数对的和的最大值。
- 代码三步曲:
- 算每个数字的 "数位最大数字"(确定类别);
- 把数字加入对应类别,更新该类别的最大、次大值;
- 遍历所有类别,计算有效数对的和的最大值。
- 记住那个 "运动会项目" 的比喻,就能把抽象的代码逻辑转化成直观的场景,就不会懵了。
3 题目
给你一个下标从0 开始的整数数组 nums 。如果 i < j 且 j - 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 <= 1051 <= 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;
}
};
代码关键部分解释
- 总数计算 :用
(long long)n * (n - 1) / 2是为了避免整数溢出(n 可能很大,比如 1e5,n*(n-1) 会超过 int 的范围,所以先转 long long)。 - key 的定义 :
key = nums[k] - k,这是从数学变形中得到的核心标识。 - 好数对统计逻辑 :
- 遍历每个下标 k 时,
keyCount[key]表示在 k 之前 ,有多少个下标 i 的 key 和当前 k 的 key 相同。这些 i 都能和 k 组成好数对(因为 i < k),所以先把这个数量加到goodPairs里。 - 然后把当前 key 的计数加 1,供后续下标使用。
- 遍历每个下标 k 时,
- 最终结果:总数减去好数对数量,就是坏数对数量。
举个例子帮你理解(比如 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]++; //这两行什么意思?
这两行是统计好数对 的核心逻辑,也是最容易绕晕的地方,我用通俗的语言 + 具体例子给你讲透,保证你能懂。
先明确两个前提
- 好数对的条件 :下标
i < k(注意这里把原来的j换成了k,只是变量名变了),且nums[i]-i = nums[k]-k(也就是两个下标对应的key相等)。 keyCount[key]的含义 :在遍历到当前下标k时,keyCount[key]存储的是在k之前(也就是i < k),有多少个下标i的key和当前k的key相等。
逐行解释
第一行:goodPairs += keyCount[key];
这句话的意思是:把当前k能和前面的i组成的好数对数量,加到总好数对里。
比如,当遍历到k时,keyCount[key]是 3,说明前面有 3 个下标i的key和k的一样,那么k就能和这 3 个i分别组成 3 个好数对,所以总好数对要加 3。
第二行:keyCount[key]++;
这句话的意思是:把当前k的key加入统计,让后续的下标能找到它。
因为后续会遍历到k+1、k+2等下标,这些下标需要知道 "前面有多少个和自己key相同的下标",所以要把当前k的key计数加 1。
用具体例子一步一步走(最直观)
还是用之前的例子:nums = [4,1,3,3],我们逐个遍历下标k,看这两行的执行过程:
初始状态
goodPairs = 0keyCount是空的(没有任何key的统计)
1. 遍历k=0,nums[0]=4
key = 4 - 0 = 4- 此时
keyCount[4] = 0(前面没有任何下标),所以goodPairs += 0→goodPairs还是 0。 - 然后
keyCount[4]++→keyCount[4] = 1(现在统计里有了 k=0 的 key)。
2. 遍历k=1,nums[1]=1
key = 1 - 1 = 0- 此时
keyCount[0] = 0,所以goodPairs += 0→ 还是 0。 - 然后
keyCount[0]++→keyCount[0] = 1(统计里有了 k=1 的 key)。
3. 遍历k=2,nums[2]=3
key = 3 - 2 = 1- 此时
keyCount[1] = 0,所以goodPairs += 0→ 还是 0。 - 然后
keyCount[1]++→keyCount[1] = 1(统计里有了 k=2 的 key)。
4. 遍历k=3,nums[3]=3
key = 3 - 3 = 0- 此时
keyCount[0] = 1(前面有 k=1 的 key 是 0),所以goodPairs += 1→goodPairs变成 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),结果就错了。
所以必须先累加前面的数量,再把自己加入统计,这是关键顺序。
总结
goodPairs += keyCount[key]:统计当前下标k能和前面的下标组成的好数对数量,加到总数里。keyCount[key]++:把当前下标k的key加入统计,供后续下标使用。- 这两行的顺序不能颠倒,否则会错误地把当前下标自己算进去,导致结果出错。
- 核心优化点是数学公式变形 (将坏数对条件转化为好数对的
nums[k]-k相等)和反向统计(总数减好数对),避免了嵌套循环。 - unordered_map 的作用是统计每个 key 的出现次数,从而快速计算好数对数量,时间复杂度降到 (O(n))。
- 注意整数溢出问题,需要用
long long类型存储总数、好数对数量等变量。