题解:P6696 [BalticOI 2020] 图 (Day2)

随机跳题跳到了这一道。然后花了将近一天调出来了,嗯兑。

题目传送门

题目描述

有一个无向图,每条边都有一种颜色:红或者黑。为每个节点配一个 实数点权,使得:

  • 对于每条黑色边,两个端点的点权之和为 \(1\)
  • 对于每条红色边,两个端点的点权之和为 \(2\)
  • 所有点权的绝对值之和是最小的。

构造一种点权的分配方案。

对于 \(100\%\) 的数据,\(1 \le N \le 10^5\)\(0 \le M \le 2 \times 10^5\)

题目分析

先避几个坑:

  1. 赋值给点以点权,不是边权;
  2. 实数 点权,也就是说 可以是小数或负数
  3. 要求绝对值最小。

不难想到,若已知一点点权为 \(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\),我们分类讨论:

  1. \(|S|\) 是奇数时,最优点即为集合 \(S\) 的中位数 \(S_{mid}\)
  2. \(|S|\) 是偶数时,此时集合 \(S\) 的中位数不在集合内,令中位数为 \(\frac{S_{mid}+S_{mid+1}}{2}\),那么最优点为满足 \(S_{mid}\le x\le S_{mid+1}\) 的任意一点。

为了方便计算以及避免小数精度问题,对于第二种情况我们可以同样取 \(S_{mid}\)

剩下的一些小细节会在代码中说明。

总结

  1. 首先对于每一个连通块随便钦定一个点跑一遍 DFS,\(\mathcal{O}(m)\) 求正确性并构造唯一解,顺便记录中位数需要的数据;
  2. 若无法构造唯一解,排序 \(\mathcal{O}(n\log n)\) 求出最优解(中位数)。当然可以用 std::nth_element \(\mathcal{O}(n)\) 求中位数,但是太过繁琐也不易理解 我懒得写了
  3. 带入每一个点的函数 \(\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;
}

提交记录

画图不易,还望多多支持


\[\color{green} The\;End \]

posted @ 2026-08-27 16:17  听凭风引adj  阅读(11)  评论(1)    收藏  举报