2020 - Graph
Solución en video
Por David Zhou
Nota: la solución en video puede no coincidir con las demás. Código en C++ y Java.
Video de YouTube (CcJCrqD6D64)
Solución
Explicación
Propagación de restricciones
Supongamos que el valor de un extremo de una arista es . Si , entonces podemos hallar el valor del otro extremo como . Con base en esto, si fijamos el valor de un nodo en una componente conexa, todos los demás valores de esa componente quedan determinados siguiendo las aristas.
Determinar los valores generales de los nodos
Para cada componente conexa, podemos expresar todos los valores de los nodos en términos de una sola variable :
- Empezamos con un nodo arbitrario y le asignamos el valor
- Luego propagamos por las aristas. Si y la arista tiene suma , entonces
Esto significa que el valor de cada nodo se representa de la forma , donde .
Comprobar factibilidad en componentes conexas cíclicas
Encontramos un ciclo cuando visitamos un nodo ya visitado. Esto significa que hay que comprobar factibilidad.
- Si volvemos a llegar al nodo , tenemos dos expresiones para él
- El ciclo nos da una ecuación en términos de
- Si la ecuación no tiene solución o contradice una solución previa, el problema es infactible
Por qué las ecuaciones de ciclo determinan x
Consideremos un camino del nodo de vuelta a sí mismo. Sea nuestro valor inicial para la propagación. Más adelante, si volvemos en un bucle, obtendremos otra expresión .
Caso 1: : Obtenemos . Si , la componente conexa es infactible, así que el grafo entero es infactible.
Caso 2: : Podemos resolver: , lo que determina de forma única un valor para .
Componentes conexas acíclicas
Cuando ningún ciclo determina , hay que elegir su valor para minimizar la expresión: . Esto se puede hacer poniendo como la mediana de las raíces, porque este problema se reduce a minimizar la suma de diferencias absolutas.
Por qué la mediana minimiza la suma
Queremos minimizar la función .
Como cada , podemos reescribir cada término como , donde
Así, minimizar es equivalente a minimizar
De forma intuitiva, podemos pensar en intentar empujar lo más cerca posible del medio de las raíces, porque colocarlo ahí “equilibra” los valores.
Matemáticamente, podemos fijar cualquier y un incremento positivo pequeño de modo que ninguna quede en el intervalo .
Consideremos el cambio .
Para cada , ocurre una de dos cosas:
- Si , entonces .
- Si , entonces .
Por lo tanto, .
Sean y . Nótese que .
Si entonces , así que moverse un poco a la derecha disminuye .
Si entonces , así que moverse un poco a la derecha aumenta . De forma equivalente, moverse a la izquierda disminuye .
Si entonces . Los movimientos no afectan el valor de .
Por lo tanto, los únicos mínimos posibles ocurren en puntos donde y . Esto significa o bien o que moverse en cualquiera de las dos direcciones deja los conteos equilibrados. Esto es cierto en la mediana de las raíces.
Determinar los valores reales de los nodos
Independientemente de cómo hayamos computado , una vez que lo tenemos, hay que propagar este valor por las componentes conexas. Podemos sustituir en cada y determinar el valor exacto de cada nodo que minimiza la suma de sus valores absolutos.
Implementación
Complejidad temporal:
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
const ll INF = 1e18;
vector<vector<pll>> adj;
vector<pll> coeffs; // el valor de cada nodo se representa como ax + b (guardado como par {a, b})
vector<bool> vis;
vector<double> ans;
vector<int> current_component;
bool impossible;
ll current_val;
void dfs(int u) {
current_component.push_back(u);
vis[u] = true;
for (auto [v, w] : adj[u]) {
if (vis[v]) {
// hallamos un ciclo -> comprobamos consistencia o resolvemos para x
pll res = {coeffs[u].first + coeffs[v].first,
coeffs[u].second + coeffs[v].second};
if (res.first == 0) {
// los términos en x se cancelan -> comprobamos si las constantes coinciden
if (res.second != w) { impossible = true; }
} else {
// podemos resolver para x
ll expected;
if (res.first == 2) {
expected = w - res.second;
} else {
expected = res.second - w;
}
if (current_val == INF) {
current_val = expected;
} else if (current_val != expected) {
// contradicción con el x determinado previamente
impossible = true;
return;
}
}
} else {
// computamos el valor del vecino según la arista: v = w - u
coeffs[v] = {-coeffs[u].first, w - coeffs[u].second};
dfs(v);
}
}
}
void propagate(int u) {
// una vez conocido x, propagamos los valores reales por la componente
vis[u] = true;
for (auto [v, w] : adj[u]) {
if (!vis[v]) {
ans[v] = w - ans[u];
propagate(v);
}
}
}
double find_median(const vector<int> &comp) {
// el x óptimo minimiza la suma de |ax+b|, que ocurre en la mediana de las raíces
vector<ll> roots;
for (int node : comp) {
if (coeffs[node].first == 1) {
roots.push_back(-coeffs[node].second);
} else {
roots.push_back(coeffs[node].second);
}
}
sort(roots.begin(), roots.end());
int sz = roots.size();
return (roots[(sz - 1) / 2] + roots[sz / 2]) / 2.0;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
adj.resize(n);
coeffs.assign(n, {-INF, -INF});
vis.assign(n, false);
ans.resize(n);
impossible = false;
// construimos la lista de adyacencia
for (int i = 0; i < m; i++) {
int a, b;
ll c;
cin >> a >> b >> c;
--a, --b;
adj[a].push_back({b, c});
adj[b].push_back({a, c});
}
vector<vector<int>> components;
vector<ll> component_vals;
// hallamos todas las componentes conexas
for (int i = 0; i < n; i++) {
if (!vis[i]) {
current_component.clear();
current_val = INF;
coeffs[i] = {1, 0}; // el primer nodo = x
dfs(i);
components.push_back(current_component);
component_vals.push_back(current_val);
}
}
if (impossible) {
cout << "NO\n";
return 0;
}
cout << "YES\n";
fill(vis.begin(), vis.end(), false);
// computamos los valores finales de cada componente
for (int i = 0; i < components.size(); i++) {
int start = components[i][0];
if (component_vals[i] == INF) {
// x no está fijo -> usamos la mediana para la suma óptima
ans[start] = find_median(components[i]);
} else {
// x quedó determinado por un ciclo
ans[start] = component_vals[i] / 2.0;
}
propagate(start);
}
for (int i = 0; i < n; i++) {
cout << fixed << setprecision(1) << ans[i];
if (i == n - 1) {
cout << '\n';
} else {
cout << ' ';
}
}
}import java.io.*;
import java.util.*;
public class Graph {
static final long INF = (long)1e18;
static List<List<Pair>> adj;
static List<Pair> coeffs; // valor del nodo representado como ax + b (guardado como {a, b})
static boolean[] vis;
static double[] ans;
static List<Integer> currentComponent;
static boolean impossible;
static long currentVal;
static class Pair {
long first, second;
Pair(long first, long second) {
this.first = first;
this.second = second;
}
}
static void dfs(int u) {
currentComponent.add(u);
vis[u] = true;
for (Pair edge : adj.get(u)) {
int v = (int)edge.first;
long w = edge.second;
if (vis[v]) {
// hallamos un ciclo -> comprobamos consistencia o resolvemos para x
Pair res = new Pair(coeffs.get(u).first + coeffs.get(v).first,
coeffs.get(u).second + coeffs.get(v).second);
if (res.first == 0) {
// los términos en x se cancelan -> comprobamos si las constantes coinciden
if (res.second != w) impossible = true;
} else {
// podemos resolver para x
long expected;
if (res.first == 2) {
expected = w - res.second;
} else {
expected = res.second - w;
}
if (currentVal == INF) {
currentVal = expected;
} else if (currentVal != expected) {
// contradicción con el x determinado previamente
impossible = true;
return;
}
}
} else {
// computamos el valor del vecino según la arista: v = w - u
coeffs.set(v, new Pair(-coeffs.get(u).first, w - coeffs.get(u).second));
dfs(v);
}
}
}
static void propagate(int u) {
// una vez conocido x, propagamos los valores reales por la componente
vis[u] = true;
for (Pair edge : adj.get(u)) {
int v = (int)edge.first;
long w = edge.second;
if (!vis[v]) {
ans[v] = w - ans[u];
propagate(v);
}
}
}
static double findMedian(List<Integer> comp) {
// el x óptimo minimiza la suma de |ax+b|, que ocurre en la mediana de las raíces
List<Long> roots = new ArrayList<>();
for (int node : comp) {
if (coeffs.get(node).first == 1) {
roots.add(-coeffs.get(node).second);
} else {
roots.add(coeffs.get(node).second);
}
}
Collections.sort(roots);
int sz = roots.size();
return (roots.get((sz - 1) / 2) + roots.get(sz / 2)) / 2.0;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
PrintWriter pw = new PrintWriter(System.out);
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
adj = new ArrayList<>();
coeffs = new ArrayList<>();
vis = new boolean[n];
ans = new double[n];
impossible = false;
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
coeffs.add(new Pair(-INF, -INF));
}
// construimos la lista de adyacencia
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken()) - 1;
int b = Integer.parseInt(st.nextToken()) - 1;
long c = Long.parseLong(st.nextToken());
adj.get(a).add(new Pair(b, c));
adj.get(b).add(new Pair(a, c));
}
List<List<Integer>> components = new ArrayList<>();
List<Long> componentVals = new ArrayList<>();
// hallamos todas las componentes conexas
for (int i = 0; i < n; i++) {
if (!vis[i]) {
currentComponent = new ArrayList<>();
currentVal = INF;
coeffs.set(i, new Pair(1, 0)); // el primer nodo = x
dfs(i);
components.add(new ArrayList<>(currentComponent));
componentVals.add(currentVal);
}
}
if (impossible) {
pw.println("NO");
pw.close();
return;
}
pw.println("YES");
Arrays.fill(vis, false);
// computamos los valores finales de cada componente
for (int i = 0; i < components.size(); i++) {
int start = components.get(i).get(0);
if (componentVals.get(i) == INF) {
// x no está fijo -> usamos la mediana para la suma óptima
ans[start] = findMedian(components.get(i));
} else {
// x quedó determinado por un ciclo
ans[start] = componentVals.get(i) / 2.0;
}
propagate(start);
}
for (int i = 0; i < n; i++) {
pw.printf("%.1f", ans[i]);
if (i == n - 1) {
pw.println();
} else {
pw.print(" ");
}
}
pw.close();
}
}