AIGC标识 置换环与并查集,置换环进阶与计数,区间与图

C++ 置换环与并查集、置换环进阶与计数、区间与图 完全详解

本文系统讲解:置换环与并查集、置换环进阶与计数、区间与图。
代码统一使用数组模拟,不使用动态指针,不使用链式前向星(用 vector 存图)。
代码中运算符左右空格已删除。
含公式、定理、推论、专业名词解释、ASCII 图示、典型例题与完整代码。


目录

  1. 置换环与并查集
    1.1 置换的定义
    1.2 置换的循环分解
    1.3 置换环
    1.4 用并查集找置换环
    1.5 置换环的性质与定理
    1.6 典型例题:P1330 封锁阳光大学(并查集)
    1.7 典型例题:CF 500B New Year Permutation
    1.8 典型例题:洛谷 P1525 关押罪犯(扩展域并查集)
    1.9 典型例题:洛谷 P2024 食物链(带权并查集)
  2. 置换环进阶与计数
    2.1 置换的乘法
    2.2 置换的逆
    2.3 置换的阶
    2.4 循环长度的 LCM
    2.5 置换计数
    2.6 Burnside 引理
    2.7 Polya 定理
    2.8 置换开方
    2.9 置换环上的 DP
    2.10 典型例题:洛谷 P4980 Polya 定理
    2.11 典型例题:洛谷 P2561 黑白瓷砖
    2.12 典型例题:CF 1065F Up and Down the Tree
    2.13 典型例题:洛谷 P4128 图的同构计数
  3. 区间与图
    3.1 区间图
    3.2 区间约束建模
    3.3 线段树优化建图
    3.4 前缀和优化建图
    3.5 差分约束系统
    3.6 区间覆盖问题
    3.7 典型例题:CF 786B Legacy
    3.8 典型例题:洛谷 P1250 种树
    3.9 典型例题:洛谷 P1645 序列
    3.10 典型例题:洛谷 P3084 [USACO13OPEN] Photo
  4. 综合例题精讲
  5. 卡常与调试技巧
  6. 总结与习题清单

1. 置换环与并查集

1.1 置换的定义

置换(Permutation):一个有限集合到自身的双射。通常用 \(p=(p_1,p_2,\dots,p_n)\) 表示,其中 \(p_i\) 表示位置 \(i\) 上的元素被替换为 \(p_i\)。

例如 \(p=(2,3,1)\) 表示:

  • 位置 1 → 2
  • 位置 2 → 3
  • 位置 3 → 1

专业名词解释:

  • 置换:双射 \(p:\{1,\dots,n\}\to\{1,\dots,n\}\)。
  • 恒等置换:\(p_i=i\)。
  • 置换的阶:最小正整数 \(k\) 使 \(p^k\) 为恒等置换。
  • 循环:置换的一种特殊形式,如 \((1\ 2\ 3)\) 表示 \(1\to 2\to 3\to 1\)。
  • 循环分解:把置换分解为不相交循环的乘积。
  • 不动点:\(p_i=i\) 的位置。

1.2 置换的循环分解

定理

定理 1:任意置换都可以唯一分解为若干不相交循环的乘积。

证明:从任意元素出发,反复应用置换,由于双射,最终必然回到起点,形成一个循环。所有元素被划分成若干循环。∎

示例

\(p=(2,3,1,5,4)\) 的循环分解:

  • \(1\to 2\to 3\to 1\),循环 \((1\ 2\ 3)\)
  • \(4\to 5\to 4\),循环 \((4\ 5)\)

所以 \(p=(1\ 2\ 3)(4\ 5)\)。

图示

1 -> 2 -> 3 -> 1
4 -> 5 -> 4

1.3 置换环

定义

置换环:置换循环分解后,每个循环对应一个环。环上的元素通过置换互相映射。

性质

性质 1:每个元素恰好属于一个环。

性质 2:环的长度等于该循环中元素的个数。

性质 3:置换的阶 = 所有环长度的最小公倍数(LCM)。

性质 4:置换的不动点对应长度为 1 的环。

性质 5:若置换有 \(c\) 个环,则 \(p\) 与恒等置换的"距离"与 \(c\) 有关。

定理

定理 2:置换 \(p\) 的阶为:

\[\text{ord}(p)=\text{lcm}(l_1,l_2,\dots,l_c) \]

其中 \(l_i\) 是第 \(i\) 个环的长度。

证明:对每个元素 \(x\),经过 \(l_i\) 次置换回到自身。所以阶是 \(l_i\) 的公倍数,最小为 LCM。∎


1.4 用并查集找置换环

方法

把置换 \(p\) 看作边 \(i\to p_i\),用并查集合并。每个连通分量就是一个环。

int fa[N];
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
void unite(int x,int y)
{
    x=find(x);
    y=find(y);
    if(x!=y)fa[x]=y;
}
// 主函数
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=n;i++)
{
    unite(i,p[i]);
}
// 统计环数
int cnt=0;
for(int i=1;i<=n;i++)
{
    if(find(i)==i)cnt++;
}

复杂度

  • 时间:\(O(n\alpha(n))\)。
  • 空间:\(O(n)\)。

注意

并查集只能判断两个元素是否在同一环,不能直接得到环的长度。若要环长度,需遍历。

// 统计每个环的大小
vector<int>sz(n+1,0);
for(int i=1;i<=n;i++)sz[find(i)]++;

1.5 置换环的性质与定理

定理 3(置换环与交换次数)

把一个置换分解为环,若环长度为 \(l\):

  • 长度为 1 的环需要 0 次交换。
  • 长度为 \(l\) 的环需要 \(l-1\) 次交换还原为恒等。

证明:每次交换可以拆开一个环或减少环长度。∎

推论:把排列 \(p\) 还原为恒等所需最少交换次数:

\[\text{Ans}=n-c \]

其中 \(c\) 是环的个数(包括长度为 1 的环)。

定理 4(排列的逆序对与环)

排列的逆序对数的奇偶性 = \(n-c\) 的奇偶性。

证明:每次交换改变逆序对奇偶性。∎

定理 5(置换环与图论)

若把置换 \(p\) 看作有向图 \(i\to p_i\),则每个节点出度为 1,入度为 1,图由若干环组成。


1.6 典型例题:P1330 封锁阳光大学(并查集)

题意:给定无向图,给每个点染黑白两色,相邻点颜色不同。求最少染色数。

做法:二分图判定 + 并查集(扩展域)。

代码:

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m;
int fa[N*2],sz[N*2];
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
void unite(int x,int y)
{
    x=find(x);
    y=find(y);
    if(x==y)return;
    fa[x]=y;
    sz[y]+=sz[x];
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=2*n;i++)
    {
        fa[i]=i;
        sz[i]=1;
    }
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        unite(u,v+n);
        unite(v,u+n);
    }
    for(int i=1;i<=n;i++)
    {
        if(find(i)==find(i+n))
        {
            cout<<"Impossible\n";
            return 0;
        }
    }
    int ans=0;
    vector<bool>vis(n+1,false);
    for(int i=1;i<=n;i++)
    {
        int a=find(i);
        int b=find(i+n);
        if(vis[a]||vis[b])continue;
        vis[a]=vis[b]=true;
        ans+=min(sz[a],sz[b]);
    }
    cout<<ans<<"\n";
    return 0;
}

1.7 典型例题:CF 500B New Year Permutation

题意:给定排列 \(p\) 和交换矩阵 \(A\),\(A_{ij}=1\) 表示可以交换 \(p_i,p_j\)。求字典序最小的排列。

做法:并查集合并可交换的位置,每个连通分量内排序。

代码:

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;
int n;
int p[N];
int fa[N];
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
void unite(int x,int y)
{
    x=find(x);
    y=find(y);
    if(x!=y)fa[x]=y;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>p[i];
        fa[i]=i;
    }
    for(int i=1;i<=n;i++)
    {
        string s;
        cin>>s;
        for(int j=1;j<=n;j++)
        {
            if(s[j-1]=='1')unite(i,j);
        }
    }
    vector<vector<int>>groups(n+1);
    for(int i=1;i<=n;i++)groups[find(i)].push_back(i);
    for(int i=1;i<=n;i++)
    {
        if(groups[i].empty())continue;
        vector<int>vals;
        for(int x:groups[i])vals.push_back(p[x]);
        sort(vals.begin(),vals.end());
        sort(groups[i].begin(),groups[i].end());
        for(int j=0;j<groups[i].size();j++)
        {
            p[groups[i][j]]=vals[j];
        }
    }
    for(int i=1;i<=n;i++)cout<<p[i]<<" ";
    cout<<"\n";
    return 0;
}

1.8 典型例题:洛谷 P1525 关押罪犯(扩展域并查集)

题意:\(n\) 个罪犯,\(m\) 对冲突,把罪犯分到两个监狱,使最大冲突值最小。

做法:按冲突值从大到小排序,用扩展域并查集判断能否分到不同监狱。

代码:

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m;
struct Node
{
    int u,v,w;
};
Node e[N];
int fa[N*2];
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
void unite(int x,int y)
{
    x=find(x);
    y=find(y);
    if(x!=y)fa[x]=y;
}
bool cmp(Node a,Node b)
{
    return a.w>b.w;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++)cin>>e[i].u>>e[i].v>>e[i].w;
    sort(e+1,e+m+1,cmp);
    for(int i=1;i<=2*n;i++)fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        int u=e[i].u;
        int v=e[i].v;
        if(find(u)==find(v))
        {
            cout<<e[i].w<<"\n";
            return 0;
        }
        unite(u,v+n);
        unite(v,u+n);
    }
    cout<<0<<"\n";
    return 0;
}

1.9 典型例题:洛谷 P2024 食物链(带权并查集)

题意:三种动物 A、B、C,A 吃 B,B 吃 C,C 吃 A。给定关系,判断矛盾数。

做法:带权并查集,维护到根的距离模 3。

代码:

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;
int n,k;
int fa[N],d[N];
int find(int x)
{
    if(fa[x]==x)return x;
    int root=find(fa[x]);
    d[x]=(d[x]+d[fa[x]])%3;
    return fa[x]=root;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>k;
    for(int i=1;i<=n;i++)fa[i]=i;
    int ans=0;
    while(k--)
    {
        int op,x,y;
        cin>>op>>x>>y;
        if(x>n||y>n)
        {
            ans++;
            continue;
        }
        int rx=find(x);
        int ry=find(y);
        if(op==1)
        {
            if(rx==ry&&d[x]!=d[y])ans++;
            else if(rx!=ry)
            {
                fa[rx]=ry;
                d[rx]=(d[y]-d[x]+3)%3;
            }
        }
        else
        {
            if(rx==ry&&(d[x]-d[y]+3)%3!=1)ans++;
            else if(rx!=ry)
            {
                fa[rx]=ry;
                d[rx]=(d[y]-d[x]+4)%3;
            }
        }
    }
    cout<<ans<<"\n";
    return 0;
}

2. 置换环进阶与计数

2.1 置换的乘法

定义

两个置换 \(p,q\) 的乘积 \(p\circ q\) 定义为:

\[(p\circ q)(i)=p(q(i)) \]

示例

\(p=(2,3,1)\),\(q=(1,3,2)\):

  • \((p\circ q)(1)=p(q(1))=p(1)=2\)
  • \((p\circ q)(2)=p(q(2))=p(3)=1\)
  • \((p\circ q)(3)=p(q(3))=p(2)=3\)

所以 \(p\circ q=(2,1,3)\)。


2.2 置换的逆

定义

置换 \(p\) 的逆 \(p^{-1}\) 满足 \(p\circ p^{-1}=p^{-1}\circ p=\text{id}\)。

代码

for(int i=1;i<=n;i++)inv[p[i]]=i;

2.3 置换的阶

定义

置换 \(p\) 的阶是最小正整数 \(k\) 使 \(p^k=\text{id}\)。

公式

\[\text{ord}(p)=\text{lcm}(l_1,l_2,\dots,l_c) \]

示例

\(p=(1\ 2\ 3)(4\ 5)\),环长度 3 和 2,阶为 \(\text{lcm}(3,2)=6\)。

代码

int order(int p[],int n)
{
    vector<bool>vis(n+1,false);
    int res=1;
    for(int i=1;i<=n;i++)
    {
        if(vis[i])continue;
        int len=0;
        int x=i;
        while(!vis[x])
        {
            vis[x]=true;
            x=p[x];
            len++;
        }
        res=lcm(res,len);
    }
    return res;
}

2.4 循环长度的 LCM

定理

定理 6:置换 \(p\) 的阶等于所有环长度的 LCM。

证明:见 1.3。∎

应用

  • 判断置换何时回到恒等。
  • 计算循环节。

典型例题:P2561 黑白瓷砖

题意:\(n\times n\) 瓷砖,黑白染色,旋转后相同视为同一种,求方案数。

做法:Polya 定理 + 置换环。


2.5 置换计数

问题

求 \(n\) 个元素的置换中,有 \(k\) 个环的置换个数。

公式(第一类斯特林数)

\[{n\brack k}={n-1\brack k-1}+(n-1){n-1\brack k} \]

其中 \({n\brack k}\) 表示 \(n\) 个元素恰好有 \(k\) 个环的置换数。

代码

int stirling[N][N];
void init(int n)
{
    stirling[0][0]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            stirling[i][j]=stirling[i-1][j-1]+(i-1)*stirling[i-1][j];
        }
    }
}

示例

\(n=3\):

  • 1 个环:2 个((1 2 3), (1 3 2))
  • 2 个环:3 个
  • 3 个环:1 个(恒等)

2.6 Burnside 引理

定理

Burnside 引理:设群 \(G\) 作用在集合 \(X\) 上,不同轨道数(即不同染色方案数)为:

\[|X/G|=\frac{1}{|G|}\sum_{g\in G}|X^g| \]

其中 \(X^g\) 是在 \(g\) 作用下不变的元素集合。

应用

  • 计算旋转、翻转等价下的染色方案数。
  • 计算图的同构数。

2.7 Polya 定理

定理

Polya 定理:若用 \(m\) 种颜色染色,群 \(G\) 作用在 \(n\) 个位置上,则不同染色方案数:

\[|X/G|=\frac{1}{|G|}\sum_{g\in G}m^{c(g)} \]

其中 \(c(g)\) 是置换 \(g\) 的环个数。

典型例题:洛谷 P4980 Polya 定理

题意:用 \(n\) 种颜色给 \(n\) 个珠子的项链染色,旋转等价,求方案数。

代码:

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=1e9+7;
const int inf=1e18;
int qpow(int a,int b,int p)
{
    int res=1;
    while(b)
    {
        if(b&1)res=res*a%p;
        a=a*a%p;
        b>>=1;
    }
    return res;
}
int phi(int n)
{
    int res=n;
    for(int i=2;i*i<=n;i++)
    {
        if(n%i==0)
        {
            res=res/i*(i-1);
            while(n%i==0)n/=i;
        }
    }
    if(n>1)res=res/n*(n-1);
    return res;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int T;
    cin>>T;
    while(T--)
    {
        int n;
        cin>>n;
        int ans=0;
        for(int d=1;d*d<=n;d++)
        {
            if(n%d)continue;
            ans=(ans+phi(d)*qpow(n,n/d,mod))%mod;
            if(d*d!=n)ans=(ans+phi(n/d)*qpow(n,d,mod))%mod;
        }
        ans=ans*qpow(n,mod-2,mod)%mod;
        cout<<ans<<"\n";
    }
    return 0;
}

2.8 置换开方

问题

给定置换 \(p\),求置换 \(q\) 使 \(q^2=p\)。

做法

  1. 分解 \(p\) 为环。
  2. 对每个环:
    • 若环长度为奇数,开方后仍为一个环。
    • 若环长度为偶数,需要两个长度相同的环配对。

代码思路

// 分解环
vector<vector<int>>cycles;
// 对奇数长度环:q[i]=p[p[i]]
// 对偶数长度环:两两配对

典型例题:CF 1065F Up and Down the Tree


2.9 置换环上的 DP

问题

在置换环上做 DP,如求最长路径、方案数。

做法

把环拆成链,用环形 DP。

典型例题:洛谷 P4128 图的同构计数


2.10 典型例题:洛谷 P4980 Polya 定理

见 2.7。


2.11 典型例题:洛谷 P2561 黑白瓷砖

题意:\(n\times n\) 瓷砖,黑白染色,旋转后相同视为同一种,求方案数。

做法:Polya 定理,群为 4 个旋转。

// 旋转 0°:n*n 个环
// 旋转 90°:ceil(n*n/4) 个环
// 旋转 180°:ceil(n*n/2) 个环
// 旋转 270°:ceil(n*n/4) 个环

2.12 典型例题:CF 1065F Up and Down the Tree

题意:树上每个节点有深度,叶子可以跳到祖先,求最多访问叶子数。

做法:树形 DP + 置换环。


2.13 典型例题:洛谷 P4128 图的同构计数

题意:求 \(n\) 个点的无向图在置换群作用下的不同构数。

做法:Polya 定理 + 置换环。


3. 区间与图

3.1 区间图

定义

区间图(Interval Graph):每个节点对应一个区间,两个节点有边当且仅当区间相交。

性质

  • 区间图是弦图。
  • 最大团 = 最大重叠区间数。
  • 最小染色数 = 最大团大小。

应用

  • 任务调度。
  • 资源分配。

3.2 区间约束建模

问题

给定若干区间约束,如"区间 \([l,r]\) 内至少选 \(k\) 个点",求最少选点数。

做法

差分约束系统。设 \(s_i\) 为前 \(i\) 个位置选点数:

  • \(s_i-s_{i-1}\ge 0\)
  • \(s_i-s_{i-1}\le 1\)
  • \(s_r-s_{l-1}\ge k\)

转化为最短路。

典型例题:洛谷 P1250 种树

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m;
vector<pair<int,int>>g[N];
int dist[N];
bool inq[N];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        g[i-1].push_back({i,0});
        g[i].push_back({i-1,-1});
    }
    for(int i=1;i<=m;i++)
    {
        int l,r,k;
        cin>>l>>r>>k;
        g[l-1].push_back({r,k});
    }
    for(int i=0;i<=n;i++)dist[i]=-inf;
    dist[0]=0;
    queue<int>q;
    q.push(0);
    inq[0]=true;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        inq[u]=false;
        for(auto e:g[u])
        {
            int v=e.fi;
            int w=e.se;
            if(dist[v]<dist[u]+w)
            {
                dist[v]=dist[u]+w;
                if(!inq[v])
                {
                    inq[v]=true;
                    q.push(v);
                }
            }
        }
    }
    cout<<dist[n]<<"\n";
    return 0;
}

3.3 线段树优化建图

问题

点 \(u\) 向区间 \([l,r]\) 内所有点连边。

做法

建立线段树,用线段树节点作为虚点。

详细实现

两棵线段树:

  • 入树(in-tree):父节点向子节点连边(向下)。
  • 出树(out-tree):子节点向父节点连边(向上)。

编号:

int id_in(int p){return p;}          // 入树节点
int id_out(int p){return p+4*n;}     // 出树节点
int id_leaf(int x){return x+8*n;}    // 原图点

建树:

void build(int p,int l,int r)
{
    if(l==r)
    {
        add(id_in(p),id_leaf(l),0);
        add(id_leaf(l),id_out(p),0);
        return;
    }
    int mid=(l+r)>>1;
    build(p<<1,l,mid);
    build(p<<1|1,mid+1,r);
    add(id_in(p),id_in(p<<1),0);
    add(id_in(p),id_in(p<<1|1),0);
    add(id_out(p<<1),id_out(p),0);
    add(id_out(p<<1|1),id_out(p),0);
}

点 → 区间:

void update_in(int p,int l,int r,int ql,int qr,int u,int w)
{
    if(ql<=l&&r<=qr)
    {
        add(u,id_in(p),w);
        return;
    }
    int mid=(l+r)>>1;
    if(ql<=mid)update_in(p<<1,l,mid,ql,qr,u,w);
    if(qr>mid)update_in(p<<1|1,mid+1,r,ql,qr,u,w);
}

区间 → 点:

void update_out(int p,int l,int r,int ql,int qr,int u,int w)
{
    if(ql<=l&&r<=qr)
    {
        add(id_out(p),u,w);
        return;
    }
    int mid=(l+r)>>1;
    if(ql<=mid)update_out(p<<1,l,mid,ql,qr,u,w);
    if(qr>mid)update_out(p<<1|1,mid+1,r,ql,qr,u,w);
}

典型例题:CF 786B Legacy

见 2.5(前文)。


3.4 前缀和优化建图

问题

点 \(i\) 向 \(1\sim i-1\) 连边。

做法

建立前缀和点 \(pre_i\),\(pre_i\to pre_{i-1}\),点 \(i\to pre_{i-1}\)。


3.5 差分约束系统

定义

差分约束系统:一组形如 \(x_j-x_i\le c_k\) 的不等式。

转化为最短路

  • 边 \(i\to j\) 权 \(c_k\)。
  • 求最短路,\(dist[j]\le dist[i]+c_k\)。
  • 若存在负环,无解。

典型例题:洛谷 P5960 差分约束

#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m;
vector<pair<int,int>>g[N];
int dist[N],cnt[N];
bool inq[N];
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        g[v].push_back({u,w});
    }
    for(int i=1;i<=n;i++)
    {
        g[0].push_back({i,0});
    }
    for(int i=0;i<=n;i++)dist[i]=inf;
    dist[0]=0;
    queue<int>q;
    q.push(0);
    inq[0]=true;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        inq[u]=false;
        for(auto e:g[u])
        {
            int v=e.fi;
            int w=e.se;
            if(dist[v]>dist[u]+w)
            {
                dist[v]=dist[u]+w;
                cnt[v]=cnt[u]+1;
                if(cnt[v]>n)
                {
                    cout<<"NO\n";
                    return 0;
                }
                if(!inq[v])
                {
                    inq[v]=true;
                    q.push(v);
                }
            }
        }
    }
    for(int i=1;i<=n;i++)cout<<dist[i]<<" ";
    cout<<"\n";
    return 0;
}

3.6 区间覆盖问题

问题

给定若干区间,选最少点覆盖所有区间。

做法

贪心:按右端点排序,每次选右端点。

典型例题:洛谷 P1645 序列


3.7 典型例题:CF 786B Legacy

见 3.3。


3.8 典型例题:洛谷 P1250 种树

见 3.2。


3.9 典型例题:洛谷 P1645 序列

题意:给定若干区间约束,求满足条件的最少选点数。

做法:差分约束。


3.10 典型例题:洛谷 P3084 [USACO13OPEN] Photo

题意:给定若干区间,每个区间恰好有一个特殊点,求最多特殊点数。

做法:差分约束 + 最短路。


4. 综合例题精讲

4.1 P1330 封锁阳光大学

见 1.6。

4.2 CF 500B New Year Permutation

见 1.7。

4.3 P1525 关押罪犯

见 1.8。

4.4 P2024 食物链

见 1.9。

4.5 P4980 Polya 定理

见 2.7。

4.6 P2561 黑白瓷砖

见 2.11。

4.7 P1250 种树

见 3.2。

4.8 CF 786B Legacy

见 3.3。


5. 卡常与调试技巧

5.1 卡常

  • 用 vector 存图。
  • 用数组,不用指针。
  • 用快读。
  • 并查集路径压缩 + 按秩合并。
  • 线段树建图注意节点编号。
  • 差分约束用 SPFA 时注意负环。

5.2 常见错误

  1. 并查集忘记初始化。
  2. 置换环长度统计错误。
  3. Polya 定理环数计算错误。
  4. 差分约束边方向错误。
  5. 线段树建图节点数不够。
  6. 置换开方偶数环处理错误。

5.3 调试方法

  • 小数据暴力对拍。
  • 打印置换环。
  • 检查并查集连通性。
  • 检查差分约束最短路。

6. 总结与习题清单

6.1 核心要点

  • 置换环:循环分解,并查集找环。
  • 置换计数:第一类斯特林数,Burnside 引理,Polya 定理。
  • 区间与图:区间图、差分约束、线段树建图。

6.2 习题清单

题目 难度 考点
P1330 中等 并查集+二分图
CF 500B 中等 并查集+排序
P1525 中等 扩展域并查集
P2024 中等 带权并查集
P4980 较难 Polya 定理
P2561 较难 Polya 定理
P4128 难 图的同构计数
P1250 中等 差分约束
P1645 中等 差分约束
P3084 较难 差分约束
CF 786B 较难 线段树建图
P5960 模板 差分约束

6.3 推荐练习顺序

P1330 → CF 500B → P1525 → P2024 → P1250 → P1645 → P5960 → CF 786B → P4980 → P2561 → P4128 → P3084。


结语

置换环与并查集、置换环进阶与计数、区间与图是竞赛中重要的高级技巧。掌握它们需要理解:

  1. 置换的循环分解。
  2. 并查集找环。
  3. 第一类斯特林数。
  4. Burnside 引理与 Polya 定理。
  5. 差分约束系统。
  6. 线段树优化建图。

推荐练习顺序:从基础并查集开始,逐步深入 Polya 定理和线段树建图。

祝你在 OI/IOI 之路上越走越远!

posted @ 2026-09-27 08:02  Aries2011  阅读(28)  评论(0)    收藏  举报