1 条题解
-
0
D. Connect the Dots 详细题解
题目回顾
在直线上有 个点(编号 ),初始时每个点独立为一个连通分量。 进行 次操作:每次给出 (),选中点:
将这些点两两连通。 求最终的连通分量数量。
一、核心突破口
题目给出关键限制:,这是解题的核心。 直接暴力合并等差数列的点会超时,但利用 很小的特点,我们可以用并查集(DSU)+ 状态递推高效合并。
二、关键定义(数组含义)
定义常数:
- (点的最大数量)
- (因为 )
数组说明:
par[],sz[]:并查集标准数组,父节点和集合大小start_cnt[i][d]:以 为起点、步长为 的操作数量end_cnt[i][d]:以 为终点、步长为 的操作数量dp[i][d]:记录点 被步长为 的活动区间覆盖的层数ind[i][d]:点 在步长为 的连续区间内,代表的连通分量根节点
三、核心算法思想
1. 操作转化
每次操作不需要暴力合并,只需要:
- 起点 :
start_cnt[a][d]++ - 终点 :
end_cnt[end][d]++
2. 递推合并(核心逻辑)
遍历每个点 ,遍历每个步长 :
- 当前层数:
- 若前一个步长点 存在,且 :
- 说明 和 在同一个等差数列区间内
- 直接合并 和
- 继承 作为当前代表元
- 层数累加:
3. 为什么正确?
只要 ,说明 正处于一个步长为 的连续合并区间中,那么 也属于这个区间,必须和区间内的点连通。 利用 ,总复杂度仅 ,完美通过题目限制。
四、标程代码逐行解析
#include <bits/stdc++.h> #define ll long long using namespace std; const ll N = 2e5+2; const ll C = 10 + 1; // d <= 10,所以开 1~10 // 并查集 vector<ll> par(N), sz(N, 0); // dp:覆盖层数 ind:连通代表元 // start_cnt/end_cnt:起点终点计数 vector<vector<ll>> dp(N, vector<ll> (C, 0)), ind(N, vector<ll> (C, 0)); vector<vector<ll>> start_cnt(N, vector<ll> (C, 0)), end_cnt(N, vector<ll> (C,0)); // 并查集找根 ll find_par(ll a) { if (par[a] == a) return a; return par[a] = find_par(par[a]); } // 并查集合并 void unite(ll a, ll b){ a = find_par(a), b = find_par(b); if (a == b) return; if (sz[b] > sz[a]) swap(a, b); sz[a] += sz[b]; par[b] = a; } // 每组测试用例重置数组 void reset(ll n) { for (ll i = 1; i <= n; i++) { par[i] = i; sz[i] = 1; for (ll j = 1; j < C; j++) { dp[i][j] = start_cnt[i][j] = end_cnt[i][j] = 0; ind[i][j] = i; } } } void solve() { ll n, m, a, d, k; cin >> n >> m; reset(n); // 只记录起点和终点,不暴力合并 for (ll i = 0; i < m; i++) { cin >> a >> d >> k; start_cnt[a][d]++; end_cnt[a + k * d][d]++; } // 核心递推:遍历每个点,每个步长 d for (ll i = 1; i <= n; i++) { for (ll j = 1; j < C; j++) { // 当前覆盖层数 = 起点数 - 终点数 dp[i][j] = start_cnt[i][j] - end_cnt[i][j]; // 如果前一个步长点存在 if (i-j < 1) continue; // 前一个点在有效区间内 → 合并 if (dp[i-j][j]) { unite(ind[i-j][j], i); ind[i][j] = ind[i-j][j]; // 继承代表元 dp[i][j] += dp[i-j][j]; // 继承层数 } } } // 统计连通分量数量(根节点数量) ll ans = 0; for (ll i = 1; i <= n; i++) { if (find_par(i) == i) ans++; } cout << ans << "\n"; } int main() { ios::sync_with_stdio(0); cin.tie(0); // 加速输入输出 ll t; cin >> t; while (t--) solve(); }
五、算法流程总结
- 输入操作:只记录每个步长 的起点和终点。
- 递推遍历:
- 对每个点 和步长 ,计算当前覆盖层数。
- 若 处于覆盖状态,直接合并 与 的代表元。
- 统计答案:遍历所有点,统计根节点数量。
六、复杂度分析
- 时间复杂度:
- 空间复杂度:
- 完全满足 的限制
七、样例验证(第一个样例)
输入:
10 2 1 2 4 → 点 1,3,5,7,9 2 2 4 → 点 2,4,6,8,10递推后:
- 所有奇数点连通
- 所有偶数点连通 最终连通分量 = ,与样例输出一致。
总结
- 核心利用:,采用递推避免暴力合并。
- 关键思路:用
dp记录区间覆盖,用ind记录连通代表元。 - 高效合并:仅遍历 次,并用并查集维护连通性。
- 标准答案:代码与标程完全一致,可直接AC。
- 1
信息
- ID
- 6336
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者