题解:P6696 [BalticOI 2020] 图 (Day2)
随机跳题跳到了这一道。然后花了将近一天调出来了,嗯兑。
题目描述
有一个无向图,每条边都有一种颜色:红或者黑。为每个节点配一个 实数点权,使得:
- 对于每条黑色边,两个端点的点权之和为 \(1\);
- 对于每条红色边,两个端点的点权之和为 \(2\);
- 所有点权的绝对值之和是最小的。
构造一种点权的分配方案。
对于 \(100\%\) 的数据,\(1 \le N \le 10^5\),\(0 \le M \le 2 \times 10^5\)。
题目分析
先避几个坑:
- 赋值给点以点权,不是边权;
- 是 实数 点权,也就是说 可以是小数或负数;
- 要求绝对值最小。
不难想到,若已知一点点权为 \(x_i\),那么与之相连的另一点一定可以用边权减去该点点权表示出来。也即 \(x_j=c_{i,j}-x_i\)。
那么对于一个连通块,钦定某一点,这个连通块内部所有的点都可以被一个简单的式子表示出来。
具体地说,钦定一个点点权为 \(x\),那么与之连通的每一个点 \(i\) 都可以用一个一次函数 \(y=k_i x+b_i\) 表示,其中,\(\forall k_i\in \{-1,1\}\)。
如下图是第一个样例。
我们在一定条件下会重复遍历到一个点,假设这个点的一次函数已经被表示为了 \(kx+b\),现在遍历到这个点应为 \(k'x+b'\),能够列出方程 \(kx+b=k'x+b'\),那么有下面两种情况:
- \(k=k'\),移项能够将 \(x\) 消掉得出 \(b=b'\),当 \(b=b'\) 时有无穷多解,否则不成立,无解;
- \(k\ne k'\),移项得到 \(x=\frac{b'-b}{k-k'}\)。直接得出答案。(注意加减顺序)
其中无穷多解可以直接不管,没有任何影响。
但是也有可能是这种情况:
这个图里任意一点都没有定值,那么我们就要使所有点的点权的绝对值之和最小,也即求 \(\sum{|k_i x+b_i|}\) 最小。
由于 \(\forall k_i\in \{-1,1\}\),因此上面的式子可以化成 \(\sum{|x-b'_i|}\) 的形式,其中,\(b'_i=-k_i\times b_i\)。然后就变成了经典的坐标轴上距离和最小点问题了。
这里我认为有必要说明一下:这个式子最小解 \(x\) 仍然不一定是一个定值。
首先对于坐标轴上两点 \(x_1,x_2\) 取相距之和最小点,取在两点之间时,距离和恒为 \(|x_1-x_2|\)。其他情况一定不优,因此最优点为两点之间任意一点。
对于坐标轴上多点构成不重点集 \(S\),取距离之和最小点,记 \(mid=\lceil\frac{|S|}{2}\rceil\),我们分类讨论:
- 当 \(|S|\) 是奇数时,最优点即为集合 \(S\) 的中位数 \(S_{mid}\)。
- 当 \(|S|\) 是偶数时,此时集合 \(S\) 的中位数不在集合内,令中位数为 \(\frac{S_{mid}+S_{mid+1}}{2}\),那么最优点为满足 \(S_{mid}\le x\le S_{mid+1}\) 的任意一点。
为了方便计算以及避免小数精度问题,对于第二种情况我们可以同样取 \(S_{mid}\)。
剩下的一些小细节会在代码中说明。
总结
- 首先对于每一个连通块随便钦定一个点跑一遍 DFS,\(\mathcal{O}(m)\) 求正确性并构造唯一解,顺便记录中位数需要的数据;
- 若无法构造唯一解,排序 \(\mathcal{O}(n\log n)\) 求出最优解(中位数)。当然可以用
std::nth_element\(\mathcal{O}(n)\) 求中位数,但是太过繁琐也不易理解我懒得写了。 - 带入每一个点的函数 \(\mathcal{O}(m)\) 检验正确性并求出函数值。
时间复杂度 \(\mathcal{O}(2m+n\log n)\),忽略常数 \(\mathcal{O}(m+n\log n)\)。
Code:
//@cao2333, LGUID:1717406
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+7;
const long double E=1e-6;
int n,m;
struct Sid{
int to,w;//存边用
}; vector<Sid> v[N];
struct Point{
int k,b;//存点权
} pt[N];
long double ans[N], x=1717406;
//由于最终答案可能是 X=0,不建议初始化为 0 或以 !X 判断无解。
//可以设置为极大值。
bool vis[N],vvis[N];
vector<int> mid;//记录中位数
void getmp(int s, int k, int b){//点 s 的函数值表示为 y=kx+b
if(vis[s]){//之前已经遍历过
if(k==pt[s].k){
if(b!=pt[s].b) exit(puts("NO")*0);//无解
else return;//无穷多解
}
double kal=(b-pt[s].b)*1.0/(pt[s].k-k);//计算唯一解
if(x!=1717406 && fabs(kal-x)>E) exit(puts("NO")*0);///与之前的解冲突
else x=kal;
return;//记得回退
}
vis[s]=1;
pt[s]={k,b};
mid.push_back(-k*b);//加入中位数计算
for(const auto &i:v[s]) getmp(i.to,-k,i.w-b);//y'= w-(kx+b) = -kx+w-b
}
void getans(int s){//根据方程求出点 s 的取值
vvis[s]=1;
ans[s]=pt[s].k*1.0*x+pt[s].b;
for(const auto &i:v[s]){
if(vvis[i.to]){//已经求过的点用于判断正确性
if(fabs(ans[i.to]+ans[s]-i.w)>E) exit(puts("NO")*0);//不符合边的约束
continue;
}
getans(i.to);
}
}
int main(){
cin>>n>>m;
for(int i=1; i<=m; i++){
int a,b,c;
cin>>a>>b>>c;
v[a].push_back({b,c});
v[b].push_back({a,c});//双向边。
}
for(int i=1; i<=n; i++){
if(!vis[i]){//对于每一个连通块都要求解
x=1717406; mid.clear();//一定要初始化!
getmp(i,1,0);
if(x==1717406){//仍为初始值说明没有唯一解
sort(mid.begin(),mid.end());
x=mid[mid.size()>>1];
//由于 vector 下标从 0 开始,直接除算出的向下取整会 +1 映射到第 mid+1 个。
}
getans(i);//求每个点的值。
}
}
puts("YES");
for(int i=1; i<=n; i++){
cout<<ans[i]<<" ";
}
return 0;
}
画图不易,还望多多支持

浙公网安备 33010602011771号