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 处理一个点时:
- 用
lower_bound和upper_bound找到冲突区间对应的下标范围; - 从
set中取出这些尚未染色的点; - 染成相反颜色;
- 从
set中删除。
每个点最多被删除一次,因此复杂度约为:
O(nlogn) 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(nlogn) O(n\log n) O(nlogn)
二分图染色中,每个点最多从 set 中删除一次,因此总复杂度约为:
O(nlogn) O(n\log n) O(nlogn)
三染色时,每个数不断除以 kkk,单个数复杂度约为:
O(logkai) O(\log_k a_i) O(logkai)
整体可以通过。
十二、总结
这道题的本质是:
集合划分⟶冲突图染色 \text{集合划分} \longrightarrow \text{冲突图染色} 集合划分⟶冲突图染色
最重要的结论有三个:
- 如果图中没有边,答案为 1;
- 如果冲突图是二分图,答案为 2;
- 否则利用层数模 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;
}