CSP 第43次认证 T4:集合划分 —— 图染色与二分图判定

CSP 第43次认证 T4:集合划分 ------ 图染色与二分图判定

题意概述

给定 nnn 个互不相同的正整数 a1,a2,...,ana_1,a_2,\dots,a_na1,a2,...,an,以及参数 k≥2k\ge 2k≥2。

如果一个集合中存在两个数 x,yx,yx,y,满足题目定义的冲突关系,那么这个集合就不合法。

题目分两种模式:

  • op = 0:两个数恰好相差 kkk 倍时冲突;
  • op = 1:当

⌊xy⌋=k \left\lfloor \frac{x}{y}\right\rfloor=k ⌊yx⌋=k

时冲突。

要求把所有数划分为尽可能少的若干集合,使每个集合内部都不存在冲突。


一、把问题转化成图染色

把每个数 aia_iai 看成图中的一个点。

如果两个数不能出现在同一个集合里,就在对应的两个点之间连一条边。

于是原问题变成:

给冲突图染色,使相邻点颜色不同,并最小化颜色数。

每一种颜色就对应一个集合。

因此:

  • 1 种颜色:图中没有边;
  • 2 种颜色:图是二分图;
  • 如果不是二分图,则至少需要 3 种颜色。

二、op=0 的情况

当 op=0 时,冲突条件为

x=ky x=ky x=ky

因此每个数只可能和自己的 kkk 倍关系发生冲突。

整个图会拆成若干条链:

x, kx, k2x, k3x,... x,\ kx,\ k^2x,\ k^3x,\dots x, kx, k2x, k3x,...

例如当 k=2k=2k=2 时:

2,4,8,16,32 2,4,8,16,32 2,4,8,16,32

就是一条链。

链一定是二分图,因此答案最多为 2。

只需要对每条链交替染色:

A,B,A,B,A,... A,B,A,B,A,\dots A,B,A,B,A,...

如果整个图中没有边,则答案为 1;否则答案为 2。


三、op=1 的冲突条件

现在考虑核心情况:

⌊xy⌋=k \left\lfloor \frac{x}{y}\right\rfloor=k ⌊yx⌋=k

假设 x<yx<yx<y,那么应当写成

⌊yx⌋=k \left\lfloor \frac{y}{x}\right\rfloor=k ⌊xy⌋=k

根据下取整定义:

k≤yx<k+1 k\le \frac{y}{x}<k+1 k≤xy<k+1

两边乘以 xxx:

kx≤y<(k+1)x kx\le y<(k+1)x kx≤y<(k+1)x

这就是整道题最重要的区间关系。

也就是说:

对于一个数 xxx,所有比它大的冲突点,都位于区间

[kx,(k+1)x) [kx,(k+1)x) [kx,(k+1)x)

中。

同理,如果寻找比 xxx 小的冲突点 yyy,有

⌊xy⌋=k \left\lfloor \frac{x}{y}\right\rfloor=k ⌊yx⌋=k

于是:

ky≤x<(k+1)y ky\le x<(k+1)y ky≤x<(k+1)y

整理得:

xk+1<y≤xk \frac{x}{k+1}<y\le \frac{x}{k} k+1x<y≤kx

对于整数 yyy,可以写成:

⌊xk+1⌋+1≤y≤⌊xk⌋ \left\lfloor\frac{x}{k+1}\right\rfloor+1 \le y\le \left\lfloor\frac{x}{k}\right\rfloor ⌊k+1x⌋+1≤y≤⌊kx⌋

因此一个点的冲突点分布在两个连续区间内。


四、为什么答案为 2 等价于二分图

如果想把所有数分成两个集合 A,BA,BA,B,那么对于任意一条冲突边

x∼y x\sim y x∼y

两个端点必须分属不同集合。

假设

x∈A x\in A x∈A

那么和 xxx 冲突的点就必须放到 BBB。

沿着边继续传播:

A→B→A→B A\to B\to A\to B A→B→A→B

这正是标准的二分图染色。

可以定义:

cpp 复制代码
color[i] = -1; // 未染色
color[i] = 0;  // A
color[i] = 1;  // B

若当前点为 u,其邻居 v 染成相反颜色:

cpp 复制代码
color[v] = color[u] ^ 1;

如果某条边两端颜色相同,则说明发生冲突,这张图不是二分图。

因此:

可以分成两个集合  ⟺  冲突图是二分图 \text{可以分成两个集合} \iff \text{冲突图是二分图} 可以分成两个集合⟺冲突图是二分图


五、为什么不能暴力建图

如果枚举每一对数判断是否冲突,复杂度是:

O(n2) O(n^2) O(n2)

显然无法通过。

但由于对于一个数 xxx,邻居只会出现在两个连续数值区间:

(xk+1,xk] \left(\frac{x}{k+1},\frac{x}{k}\right] (k+1x,kx]

和

[kx,(k+1)x) [kx,(k+1)x) [kx,(k+1)x)

因此数组排序以后,这些邻居在下标上也构成连续区间。

于是可以使用 set 维护所有尚未染色的点。

BFS 处理一个点时:

  1. 用 lower_bound 和 upper_bound 找到冲突区间对应的下标范围;
  2. 从 set 中取出这些尚未染色的点;
  3. 染成相反颜色;
  4. 从 set 中删除。

每个点最多被删除一次,因此复杂度约为:

O(nlog⁡n) O(n\log n) O(nlogn)


六、为什么答案一定不超过 3

假设 x<yx<yx<y 且二者冲突,那么:

kx≤y<(k+1)x kx\le y<(k+1)x kx≤y<(k+1)x

因为题目保证:

k≥2 k\ge 2 k≥2

所以:

k+1≤k2 k+1\le k^2 k+1≤k2

于是:

kx≤y<k2x kx\le y<k^2x kx≤y<k2x

这意味着:

两个冲突的数,在以 kkk 为尺度的层次中,只可能相差 1 层或者 2 层。

因此可以按"层数模 3"进行染色。


七、三染色构造

定义一个数 xxx 的层数 h(x)h(x)h(x):

不断执行

cpp 复制代码
x /= k;

直到 x=0x=0x=0,记录执行次数。

例如:

k=2,x=13 k=2,\quad x=13 k=2,x=13

有:

text 复制代码
13 -> 6 -> 3 -> 1 -> 0

一共进行 4 次,所以:

h(13)=4 h(13)=4 h(13)=4

然后定义颜色:

color(x)=h(x) mod 3 color(x)=h(x)\bmod 3 color(x)=h(x)mod3

即:

text 复制代码
0 -> A
1 -> B
2 -> C

代码可以写成:

cpp 复制代码
int get_color(ll x){
    int cnt=0;
    while(x){
        x/=k;
        cnt++;
    }
    return cnt%3;
}

八、为什么这个三染色一定合法

如果 x<yx<yx<y 且二者冲突,那么:

kx≤y<k2x kx\le y<k^2x kx≤y<k2x

因此 yyy 相对于 xxx 的层数只会上升 1 或 2。

即:

h(y)−h(x)∈{1,2} h(y)-h(x)\in\{1,2\} h(y)−h(x)∈{1,2}

所以:

h(y)≢h(x)(mod3) h(y)\not\equiv h(x)\pmod 3 h(y)≡h(x)(mod3)

也就是说,冲突点不可能拥有相同颜色。

因此:

h(x) mod 3 h(x)\bmod 3 h(x)mod3

一定构成一个合法三染色。

于是:

χ(G)≤3 \chi(G)\le 3 χ(G)≤3

所以 op=1 时答案只可能是:

1,2,3 1,2,3 1,2,3


九、整体判断流程

text 复制代码
先判断图中是否存在冲突边
│
├─ 没有边
│   └─ 答案 = 1
│
└─ 有边
    │
    ├─ 是二分图
    │   └─ 答案 = 2
    │
    └─ 不是二分图
        └─ 答案 = 3

十、一个容易写错的地方

不能只检查排序后相邻元素是否冲突。

例如:

k=2 k=2 k=2

集合:

2,3,4 2,3,4 2,3,4

相邻元素:

⌊32⌋=1 \left\lfloor\frac{3}{2}\right\rfloor=1 ⌊23⌋=1

⌊43⌋=1 \left\lfloor\frac{4}{3}\right\rfloor=1 ⌊34⌋=1

看起来都不冲突。

但:

⌊42⌋=2 \left\lfloor\frac{4}{2}\right\rfloor=2 ⌊24⌋=2

所以 222 和 444 实际上发生冲突。

因此判断答案是否为 1 时,必须检查区间

[kx,(k+1)x) [kx,(k+1)x) [kx,(k+1)x)

中是否存在其他元素,不能只看相邻元素。


十一、复杂度分析

排序复杂度:

O(nlog⁡n) O(n\log n) O(nlogn)

二分图染色中,每个点最多从 set 中删除一次,因此总复杂度约为:

O(nlog⁡n) O(n\log n) O(nlogn)

三染色时,每个数不断除以 kkk,单个数复杂度约为:

O(log⁡kai) O(\log_k a_i) O(logkai)

整体可以通过。


十二、总结

这道题的本质是:

集合划分⟶冲突图染色 \text{集合划分} \longrightarrow \text{冲突图染色} 集合划分⟶冲突图染色

最重要的结论有三个:

  1. 如果图中没有边,答案为 1;
  2. 如果冲突图是二分图,答案为 2;
  3. 否则利用层数模 3 构造,答案为 3。

最核心的冲突区间为:

kx≤y<(k+1)x kx\le y<(k+1)x kx≤y<(k+1)x

而三染色成立的关键在于:

k+1≤k2 k+1\le k^2 k+1≤k2

从而:

kx≤y<k2x kx\le y<k^2x kx≤y<k2x

冲突点层数最多相差 2,因此按模 3 染色一定合法。

这道题最值得记住的两个建模技巧是:

  • 当题目要求把元素分组,并规定某些元素不能处于同组时,可以优先考虑转化为图染色问题;
  • 当冲突关系具有连续数值区间结构时,排序后往往可以使用 lower_bound、upper_bound、set 等数据结构优化建图和遍历。
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

const int N=5e5+100;

int n,op;
ll k;
ll a[N];
int c[N];

bool ok1(){
    for(int i=1;i<=n;i++){
        ll l,r;

        if(a[i]>a[n]/k) continue;
        l=a[i]*k;

        if(a[i]>a[n]/(k+1))
            r=a[n];
        else
            r=a[i]*(k+1)-1;

        int p=lower_bound(a+1,a+n+1,l)-a;

        if(p<=n&&a[p]<=r)
            return false;
    }
    return true;
}

bool check(vector<ll> v){
    int m=v.size();
    int j=0;

    for(int i=0;i<m;i++){
        if(j<=i) j=i+1;

        while(j<m){
            if(v[i]>v[j]/k)
                j++;
            else
                break;
        }

        if(j<m){
            if(v[i]<=v[j]/k){
                ll q=v[j]/v[i];
                if(q==k)
                    return false;
            }
        }
    }

    return true;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin>>n>>k>>op;

    for(int i=1;i<=n;i++)
        cin>>a[i];

    sort(a+1,a+n+1);

    if(op==0){
        vector<ll> g[N];
        bool vis[N]={0};

        int cnt=0;
        bool flag=false;

        for(int i=1;i<=n;i++){
            if(vis[i]) continue;

            cnt++;
            g[cnt].push_back(a[i]);
            vis[i]=true;

            ll x=a[i];

            while(x<=a[n]/k){
                ll y=x*k;

                int p=lower_bound(a+1,a+n+1,y)-a;

                if(p<=n&&a[p]==y){
                    flag=true;
                    g[cnt].push_back(y);
                    vis[p]=true;
                    x=y;
                }
                else
                    break;
            }
        }

        if(!flag){
            cout<<1<<'\n';
            cout<<n<<' ';

            for(int i=1;i<=n;i++)
                cout<<a[i]<<' ';

            cout<<'\n';
        }
        else{
            vector<ll> A,B;

            for(int i=1;i<=cnt;i++){
                for(int j=0;j<g[i].size();j++){
                    if(j%2==0)
                        A.push_back(g[i][j]);
                    else
                        B.push_back(g[i][j]);
                }
            }

            cout<<2<<'\n';

            cout<<A.size()<<' ';
            for(int i=0;i<A.size();i++)
                cout<<A[i]<<' ';
            cout<<'\n';

            cout<<B.size()<<' ';
            for(int i=0;i<B.size();i++)
                cout<<B[i]<<' ';
            cout<<'\n';
        }
    }

    else{
        if(ok1()){
            cout<<1<<'\n';
            cout<<n<<' ';

            for(int i=1;i<=n;i++)
                cout<<a[i]<<' ';

            cout<<'\n';

            return 0;
        }

        for(int i=1;i<=n;i++)
            c[i]=-1;

        set<int> s;

        for(int i=1;i<=n;i++)
            s.insert(i);

        for(int st=1;st<=n;st++){
            if(c[st]!=-1) continue;

            queue<int> q;

            c[st]=0;
            s.erase(st);
            q.push(st);

            while(!q.empty()){
                int u=q.front();
                q.pop();

                ll x=a[u];

                ll l=x/(k+1)+1;
                ll r=x/k;

                int L=lower_bound(a+1,a+n+1,l)-a;
                int R=upper_bound(a+1,a+n+1,r)-a-1;

                set<int>::iterator it=s.lower_bound(L);

                while(it!=s.end()&&(*it)<=R){
                    int v=*it;

                    c[v]=c[u]^1;
                    q.push(v);

                    set<int>::iterator tmp=it;
                    it++;
                    s.erase(tmp);
                }

                if(x<=a[n]/k){
                    l=x*k;

                    if(x>a[n]/(k+1))
                        r=a[n];
                    else
                        r=x*(k+1)-1;

                    L=lower_bound(a+1,a+n+1,l)-a;
                    R=upper_bound(a+1,a+n+1,r)-a-1;

                    it=s.lower_bound(L);

                    while(it!=s.end()&&(*it)<=R){
                        int v=*it;

                        c[v]=c[u]^1;
                        q.push(v);

                        set<int>::iterator tmp=it;
                        it++;
                        s.erase(tmp);
                    }
                }
            }
        }

        vector<ll> A,B;

        for(int i=1;i<=n;i++){
            if(c[i]==0)
                A.push_back(a[i]);
            else
                B.push_back(a[i]);
        }

        if(check(A)&&check(B)){
            cout<<2<<'\n';

            cout<<A.size()<<' ';
            for(int i=0;i<A.size();i++)
                cout<<A[i]<<' ';
            cout<<'\n';

            cout<<B.size()<<' ';
            for(int i=0;i<B.size();i++)
                cout<<B[i]<<' ';
            cout<<'\n';
        }

        else{
            vector<ll> A,B,C;

            for(int i=1;i<=n;i++){
                ll x=a[i];
                int t=0;

                while(x){
                    x/=k;
                    t++;
                }

                t%=3;

                if(t==0)
                    A.push_back(a[i]);
                else if(t==1)
                    B.push_back(a[i]);
                else
                    C.push_back(a[i]);
            }

            cout<<3<<'\n';

            cout<<A.size()<<' ';
            for(int i=0;i<A.size();i++)
                cout<<A[i]<<' ';
            cout<<'\n';

            cout<<B.size()<<' ';
            for(int i=0;i<B.size();i++)
                cout<<B[i]<<' ';
            cout<<'\n';

            cout<<C.size()<<' ';
            for(int i=0;i<C.size();i++)
                cout<<C[i]<<' ';
            cout<<'\n';
        }
    }

    return 0;
}
相关推荐
不做无法实现的梦~1 小时前
基于 PX4 的全仓库架构、无人机算法解析与开发学习规划
算法·架构·无人机
做cv的小昊1 小时前
【大模型算法自学笔记01】NLP基础知识(1.1 自注意力)
人工智能·笔记·算法·自然语言处理·大模型·llm
stolentime1 小时前
AT_arc182_a [ARC182A] Chmax Rush!题解
数据结构·c++·算法·动态规划
longlongzihan1 小时前
LeetCode 189. 轮转数组:从直观模拟到最优原地算法
c++·算法·leetcode
爱喝热水的呀哈喽1 小时前
cpp二分库函数 lambda捕获
算法
longlongzihan2 小时前
LeetCode 76. 最小覆盖子串:滑动窗口算法详解
算法·leetcode·滑动窗口
怕浪猫11 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试
无线通信科研笔记11 小时前
IEEE TVT 2026 论文精读与完整复现|相位误差如何重塑近场 RIS 的幅相响应
论文阅读·人工智能·python·算法·论文笔记
mmmmath_312 小时前
二叉树の递归遍历
算法