对局匹配
题目描述
小明喜欢在一个围棋网站上找别人在线对弈。这个网站上所有注册用户都有一个积分,代表他的围棋水平。
小明发现网站的自动对局系统在匹配对手时,只会将积分差恰好是 K 的两名用户匹配在一起。如果两人分差小于或大于 KK,系统都不会将他们匹配。
现在小明知道这个网站总共有 NN 名用户,以及他们的积分分别是 A1,A2,⋯ANA1,A2,⋯AN。
小明想了解最多可能有多少名用户同时在线寻找对手,但是系统却一场对局都匹配不起来(任意两名用户积分差不等于 KK)?
输入描述
输入描述
第一行包含两个个整数 N,KN,K。
第二行包含 NN 个整数 A1,A2,⋯ANA1,A2,⋯AN。
其中,1≤N≤105,0≤Ai≤105,0≤K≤1051≤N≤105,0≤Ai≤105,0≤K≤105。
输出描述
输出一个整数,代表答案。
输入输出样例
示例
输入
10 0
1 4 2 8 5 7 1 4 2 8
输出
6
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
总通过次数: 2604 | 总提交次数: 4404 | 通过率: 59.1%
难度: 困难 标签: 2017, 国赛, 动态规划
对局匹配问题:动态规划解法详解
🌟 算法思路
本问题要求找到最多用户同时在线,但任意两人积分差不等于K。核心思路是分组处理 + 动态规划:
- 分组策略:将用户按积分模K的余数分成K组(余数0~K-1)
- 组内处理:每组内积分值差为K的倍数,避免跨组匹配(差为K的数必同余)
- 链式结构:每组内积分排序后,相邻差为K的数形成"冲突链"
- 动态规划:在每条冲突链上求解最大独立集(不相邻节点和最大)
📝 算法步骤
-
特殊处理K=0:
- 直接统计不同积分值数量(每个值只能选1个用户)
- 例:输入
[1,1,2,2]
→ 不同值{1,2}
→ 答案=2
-
分组处理(K>0):
- 创建K个分组桶(余数0~K-1)
- 遍历每个积分
a
,放入a % K
桶中 - 例:K=2时,积分
[1,3,5]
放入余数1桶
-
组内处理:
- 对每组积分排序并合并相同值(记录出现次数)
- 按积分值差划分连续子链(差=K为连续,否则断开)
- 例:余数0组
[0,2,4,6]
→ 子链0-2-4-6
-
链上动态规划:
- 状态定义:
dp0
:不选当前节点的最大和dp1
:选择当前节点的最大和
- 状态转移:
- 不选当前:
new_dp0 = max(dp0, dp1)
- 选当前:
new_dp1 = dp0 + 当前权值
- 不选当前:
- 链尾结果:
max(dp0, dp1)
- 状态定义:
-
结果汇总:
- 累加所有组的结果
🧠 代码实现(C++)
#include <iostream> #include <vector> #include <algorithm> #include <set> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(0); int N, K; cin >> N >> K; vector<int> A(N); for (int i = 0; i < N; i++) { cin >> A[i]; } // 处理K=0的情况 if (K == 0) { set<int> distinct; for (int a : A) distinct.insert(a); cout << distinct.size() << endl; return 0; } // 创建K个分组 vector<vector<int>> groups(K); for (int a : A) { groups[a % K].push_back(a); } long long ans = 0; // 处理每个分组 for (int r = 0; r < K; r++) { if (groups[r].empty()) continue; // 组内排序 sort(groups[r].begin(), groups[r].end()); // 合并相同积分并计数 vector<pair<int, int>> arr; // (积分值, 出现次数) int cnt = 1; for (int i = 1; i < groups[r].size(); i++) { if (groups[r][i] == groups[r][i-1]) { cnt++; } else { arr.push_back({groups[r][i-1], cnt}); cnt = 1; } } arr.push_back({groups[r].back(), cnt}); // 划分子链(相邻差为K的连续段) vector<vector<pair<int, int>>> chains; vector<pair<int, int>> chain; chain.push_back(arr[0]); for (int i = 1; i < arr.size(); i++) { if (arr[i].first - arr[i-1].first == K) { chain.push_back(arr[i]); } else { chains.push_back(chain); chain = {arr[i]}; } } chains.push_back(chain); // 每条子链动态规划 for (auto& ch : chains) { if (ch.empty()) continue; long long dp0 = 0; // 不选前一个节点 long long dp1 = ch[0].second; // 选前一个节点 for (int i = 1; i < ch.size(); i++) { long long new_dp0 = max(dp0, dp1); long long new_dp1 = dp0 + ch[i].second; dp0 = new_dp0; dp1 = new_dp1; } ans += max(dp0, dp1); } } cout << ans << endl; return 0; }
📊 代码解析
-
输入处理:
- 使用
ios::sync_with_stdio(false)
加速输入 - 一维数组存储积分值
- 使用
-
K=0特判:
- 利用
set
去重统计不同积分数量
- 利用
-
分组管理:
vector<vector<int>> groups(K)
创建分组桶- 模运算
a % K
确定分组
-
组内压缩:
- 排序后合并相同积分,存储为
(值, 频次)
对 - 例:
[2,2,2,4]
→(2,3),(4,1)
- 排序后合并相同积分,存储为
-
子链划分:
- 相邻积分差=K的划为同链
- 例:
[0,2,4,10]
(K=2)→ 子链[0,2,4]
和[10]
-
动态规划优化:
- 滚动变量
dp0
/dp1
代替DP数组 - 空间复杂度O(1)每链,时间复杂度O(N)
- 滚动变量
-
🧪 实例验证
输入1:
K=0
,积分=[1,4,2,8,5,7,1,4,2,8]
- 不同值:
{1,2,4,5,7,8}
- 输出:6 ✓
- 输入2:
K=2
,积分=[0,2,4,1,3,5,7]
- 分组:
- 余0组:
[0,2,4]
→ 子链0-2-4
- DP:节点
(0,1)-(2,1)-(4,1)
- 选0+4=2
- DP:节点
- 余1组:
[1,3,5,7]
→ 子链1-3-5-7
- DP:选1+5或3+7=2
- 余0组:
- 输出:2+2=4 ✓
-
输入3:
K=3
,积分=[1,1,4,4,7,7,10]
- 余1组:
[1,1,4,4,7,7,10]
- 合并:
(1,2),(4,2),(7,2),(10,1)
- 子链:
1-4-7
(差3)和10
- 链1 DP:选1+7=4
- 链2 DP:选10=1
- 合并:
- 输出:4+1=5 ✓
-
⚠️ 注意事项
-
边界处理:
- K=0需单独处理
- 空分组直接跳过
- 单元素链直接取权值
-
数据类型:
- 结果用
long long
防溢出(最大1010) - 积分值范围0-105,用
int
存储
- 结果用
-
性能关键:
- 排序复杂度O(N log N)
- 链划分和DP均O(N)
- 整体复杂度O(N log N)
-
特殊测试点:
测试类型 测试数据 预期结果 K=0 [1,1,2,2] 2 K>最大积分 K=100000, [1,2,3] 3 全相同积分 K=1, [5,5,5] 3 不连续子链 K=2, [0,3,6,10,13] 4 超大随机数据 N=100000, K=50000 通过1s限 -
💡 优化建议
-
合并相同值优化:
// 使用map避免排序后遍历 unordered_map<int, int> freq; for (int a : groups[r]) freq[a]++; for (auto& p : freq) arr.push_back(p); sort(arr.begin(), arr.end());
-
链划分与DP合并:
long long chainDP = 0; long long prev0 = 0, prev1 = freq[arr[0]]; for (int i = 1; i < arr.size(); i++) { if (arr[i] - arr[i-1] == K) { // DP计算... } else { chainDP += max(prev0, prev1); prev0 = 0; prev1 = freq[arr[i]]; } }
-
内存优化:
- 用
vector<vector<int>>().swap(groups)
及时释放内存 - 使用
reserve()
预分配分组空间
- 用
-
并行化潜力:
- 不同分组可并行处理
- 使用OpenMP加速:
#pragma omp parallel for reduction(+:ans) for (int r=0; r<K; r++) { ... }
- 累加所有组的结果