2026牛客寒假基础算法训练营1 (FJ)
F. Permutation Counting
看上去很有意思的一道题:求满足长度为 \(n\),以及给定的 \(m\) 条约束的排列 \(p\) 的方案数。第 \(i\) 条约束 \((l_{i},r_{i},k_{i})\) 的定义:\(\max(p[l_{i}\backsim r_{i}]) = k_{i}\)。
首先分析一下这种性质对每个位置可以取什么值做了哪些约束。不难发现有两种:
- \(k_{i}\) 一定在 \(p[l_{i}\backsim r_{i}]\) 中出现
- \(p[l_{i}\backsim r_{i}]\) 一定均 \(\leq k\)。
考虑 \(k\) 值相同的若干约束的复合作用,我们可以进一步得到:\(k\) 一定在这些约束区间的交集中出现,且这些区间的并集中的每个元素一定 \(\leq k\)。 更进一步地,我们会发现这些区间的并集只能是某一个完整的区间,否则这些区间的交集一定为空,\(k\) 便没有位置放了,此时一定无解。
因此,我们不妨按照 \(k_{i}\) 对所有约束进行分类,并求出每种约束对应的交集 \([L1,R1]\) 和并集 \([L2,R2]\)。接下来我们用处理好的这些约束计数。
再回顾一下这些约束的含义:交集的约束本质上是钦定某个数一定出现在某个区间里;并集的约束本质上是钦定区间内的每个元素均 \(\leq\) 某个数,这启发我们值小的数约束是更轻的。那么,我们不妨 按 \(k_{i}\) 从小到大 依次决定每个数可以放的位置。
那么我们需要思考交集与并集分别为什么样的 \(k\) 服务。我们有结论:交集是为当前约束的 \(k\) 服务的,而并集是为 \(< k\),且没有出现在任何约束中的 \(k\) 服务的。
按照上述方式想,我们很快就有了一个线段树做法:从小到大决定 \(k\) 可以放在哪些位置,并对相应约束的区间作区间赋值,表示以后更大的数不能再放在这些位置了。交集相当于只决定一个元素可以放哪些位置,而并集相当于决定多个位置可以用剩下的哪些元素去放(因为对于当前 \(k\) 与下一个最小的 \(k'\),中间跨度可能会很大,中间跨过的这些 \(v \in [k + 1, k')\) 会受到并集约束)。
具体地,对于当前 \(k\),我们先决策 \(k\) 放在当前交集中的哪个空闲位置,然后再决定并集区间中剩下的空闲位置(注意要减 1,因为交集里在上一步里已经放过一个 \(k\) 了,而交集包含于并集,此时并集中少了一个需要放数的位置)应该用当前 \(< k\) 且还未放的哪些数字去填充。
但是线段树做法常数太大,本题 \(n\) 最大是 \(2e6\),很有可能会 T。因此我们不能直接按照上述用线段树作区间赋值的方式来做,需要换一个常数更小,更简便的方法去做。
我们会发现:当某个位置被区间赋值(不再可用)时,以后也一定不可用了。这很容易启发我们使用区间并查集来维护这些位置,这样我们就可以通过暴力遍历约束区间的方式来决定每个数可以放在哪些位置,因为每个位置最多只会被遍历到一次。总复杂度 \(O(m \log n)\)。
具体实现见 code。
J. MST Problem
看上去很经典的一道题:给定一个大小为 \(n \leq 3e5\) 的完全图,每个点有点权 \(a_{u}\),每条边 \((u, v)\) 的边权为 \(a_{u} + a_{v}\)。现在需要删除给定 \(m\) 条边,问删边后整个图的最小生成树的权重。
官解用了一个从来没听说过的做法 —— 数据结构优化 \(Prim\)。
\(Prim\) 算法的思想特别简单:以当前图中任意一点作为初始点集,每次选择 不在点集中的某个点 \(u\) 与该点集中任意一点 为端点,且边权最小的边 \(w\),并将 \(u\) 加入到点集中,重复上述过程直到所有点都加入到了点集。此时选择过的所有边构成图中的某一棵最小生成树。
显然,\(Prim\) 算法很适合于稠密图求最小生成树。
那么,在执行 \(Prim\) 算法的过程中,我们需要维护 当前未在构建连通块内的所有点 到 已在连通块内的所有点 的最小边权,与该边对应在连通块外的点。那么当一个点加入到连通块中时,首先我们需要消除掉它原来未在连通块时的影响(因为每次只需要查询未在连通块内的点的最优答案),然后需要用它的所有出边更新 未在连通块内所有点到连通块的最小边权。由于存在一些被删除的边,因此我们需要避免用这些删除的边更新答案;而某个固定端点的删除边,相当于将这些出边拆分成了与删除点数量同等数量级的区间,因此我们只需每次修改相应区间就行了。
下面的代码几乎与官解给的一模一样,自己实在是写不出来,但是看懂还是很容易的。这里线段树懒标记的使用非常灵活,并通过维护 pair 的方式,可以使得查询到最小值的同时也能直接锁定其位置,也是值得学习的一种写法。
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define fr first
#define se second
#define endl '\n'
#define pb push_back
// #define int long long
typedef long long ll;
typedef __int128 LL;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
const int maxn = 3e5 + 5;
int a[maxn];
struct SegTree{
#define lc p<<1
#define rc p<<1|1
struct Node{
int l, r;
pli ver; // 维护区间内 未在连通块中的所有点 的最小点权,以及对应的结点编号
pli edge; // 维护区间内 未在连通块中的所有点 到 在连通块内的所有点 的最小边权,以及对应的结点编号
ll tag; // 懒标记,表示该区间内 所有未在连通块中的点 与一个新加入连通块且点权为v的点连接
}tr[maxn<<2];
void pushup(int p){
tr[p].ver = min(tr[lc].ver, tr[rc].ver);
tr[p].edge = min(tr[lc].edge, tr[rc].edge);
}
void pushdown(int p){
if(tr[lc].tag > tr[p].tag){
if(tr[lc].ver.first + tr[p].tag < tr[lc].edge.first){
tr[lc].edge = {tr[lc].ver.first + tr[p].tag, tr[lc].ver.second};
}
tr[lc].tag = tr[p].tag;
}
if(tr[rc].tag > tr[p].tag){
if(tr[rc].ver.first + tr[p].tag < tr[rc].edge.first){
tr[rc].edge = {tr[rc].ver.first + tr[p].tag, tr[rc].ver.second};
}
tr[rc].tag = tr[p].tag;
}
}
void build(int p, int l, int r, auto& arr){
tr[p] = {l, r, {arr[l], l}, {arr[l] + INF, l}, INF};
if(l == r) return;
int mid = tr[p].l + tr[p].r >> 1;
build(lc, l, mid, arr);
build(rc, mid + 1, r, arr);
pushup(p);
}
// 点权为v的一点新加入到连通块中,对[l, r]内 所有未在连通块中的点u 到 连通块内任一点的最小边权 作 a[u]+v 的修改
void update(int p, int l, int r, int v){
if(l <= tr[p].l && tr[p].r <= r){
if(tr[p].tag > v) {
tr[p].tag = v;
if(tr[p].ver.first + v < tr[p].edge.first){
tr[p].edge = {tr[p].ver.first + v, tr[p].ver.second};
}
}
return;
}
pushdown(p);
int mid = tr[p].l + tr[p].r >> 1;
if(l <= mid) update(lc, l, r, v);
if(r > mid) update(rc, l, r, v);
pushup(p);
}
// 点x加入到连通块中,需要消除x本身的影响
void del(int p, int x){
if(tr[p].l == tr[p].r){
tr[p].ver = {INF, x};
tr[p].edge = {a[x] + INF, x};
return;
}
pushdown(p);
int mid = tr[p].l + tr[p].r >> 1;
if(x <= mid) del(lc, x);
else del(rc, x);
pushup(p);
}
pli querymin(int p, int l, int r){
if(l <= tr[p].l && tr[p].r <= r){
return tr[p].edge;
}
pushdown(p);
int mid = tr[p].l + tr[p].r >> 1;
pli res = {INF, INF};
if(l <= mid) res = min(res, querymin(lc, l, r));
if(r > mid) res = min(res, querymin(rc, l, r));
return res;
}
}seg;
void solve()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
vector<set<int>> G(n + 1);
for(int i = 1; i <= m; i ++){
int u, v;
cin >> u >> v;
G[u].insert(v); G[v].insert(u);
}
for(int i = 1; i <= n; i ++){
G[i].insert(i); // 自己也需要被删除
}
seg.build(1, 1, n, a);
int idx = 1; // 随意选一个初始结点加入到连通块中
ll ans = 0;
for(int t = 0; t < n - 1; t ++){ // 执行 n - 1 次 merge
seg.del(1, idx);
int l = 0;
for(int r : G[idx]){
if(l + 1 <= r - 1){
seg.update(1, l + 1, r - 1, a[idx]);
}
l = r;
}
if(l + 1 <= n){
seg.update(1, l + 1, n, a[idx]);
}
ans += seg.tr[1].edge.first;
idx = seg.tr[1].edge.second;
if(ans >= INF) break;
}
if(ans >= INF){
cout << -1 << "\n";
}
else{
cout << ans << "\n";
}
}
signed main()
{
// freopen("in.txt", "rt", stdin);
// freopen("out.txt", "wt", stdout);
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int T=1; cin>>T; while(T--)
solve();
return 0;
}

浙公网安备 33010602011771号