【Leetcode】465. Optimal Account Balancing

该博客讨论了如何解决LeetCode上的一个图论问题,即在给定交易记录的情况下,计算最少需要多少次转账才能使所有账户的度(出度-入度)变为零。解决方案涉及构建有向图,计算每个节点的度,然后使用位运算和动态规划找到和为零的子集,以确定最少转账次数。时间复杂度和空间复杂度也进行了分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目地址:

https://leetcode.com/problems/optimal-account-balancing/

给定一个非负权有向图,其有 n n n个顶点和 m m m条边,给出所有的边的信息。每个顶点的“度”定义为其所有入边的权值总和与所有出边的权值总和之差。问整个图至少要加多少条边可以使得每个顶点的度都恰好为 0 0 0,边的权值可以任意赋值。注意点的编号不一定是 0 ∼ n − 1 0\sim n-1 0n1,甚至不一定是连续的。

首先,对于一个这样的图而言,最多添加 n − 1 n-1 n1条边是一定可以达到效果的。由于这样的图的所有点总度数一定等于 0 0 0,先略过所有度等于 0 0 0的点,然后依次从度不为 0 0 0的点出发,向另一个度不为 0 0 0的点连边,每个点的边权设置为能使得出发点的度为 0 0 0(边的方向也要做对应的调整)。由于所有点的度总和是 0 0 0,所以这样连接了 n − 1 n-1 n1条边之后一定能使得每个点的度都为 0 0 0

接下来考虑至少要加多少边。先用离散化的方式把每个点视为编号是 0 ∼ n − 1 0\sim n-1 0n1。接下来用状态压缩,将 x x x的各个二进制位视为是这些点,二进制位取 1 1 1的时候说明含这个点,否则是不含。设 f [ x ] f[x] f[x]是在这个图里只考虑含 x x x的子图,至少需要加多少条边(这里的”子图“只含顶点,并且考虑每个顶点的度,不考虑边的情况)。设 g [ x ] g[x] g[x] x x x二进制表示里 1 1 1的个数。有两种情况,一种是最少加边方式只形成一个连通块,此时在度数总和是 0 0 0的情况下,最少加边数是 g [ x ] − 1 g[x]-1 g[x]1(度数总和如果不是 0 0 0的话,无论怎么加边都不可能使得度数总和变成 0 0 0,从而更不可能让每个点的度变成 0 0 0);另一种是最少加边方式形成了多个连通块,那么此时一定存在 x x x划分的两个子集,每个子集的度总和是 0 0 0,并且分别在两个子集内部加边,此时方案一定 g [ x ] − 1 g[x]-1 g[x]1更优,可以枚举来找到加边最少的方案。综上,有: f [ x ] = min ⁡ { g [ x ] − 1 , min ⁡ y ⊂ x { f [ y ] + f [ x − y ] } } f[x]=\min\{g[x]-1,\min_{y\subset x}\{f[y]+f[x-y]\}\} f[x]=min{g[x]1,yxmin{f[y]+f[xy]}}代码如下:

class Solution {
 public:
  int minTransfers(vector<vector<int>>& ts) {
    unordered_map<int, int> mp;
    // 求一下每个点的度
    for (auto& t : ts) {
      mp[t[0]] -= t[2];
      mp[t[1]] += t[2];
    }

    vector<int> a;
    for (auto& [k, v] : mp)
      if (v) a.push_back(v);
    // 如果已经每个点度都是0了,那不需要加边
    if (a.empty()) return 0;
    int n = a.size();
	// f[x]表示子集x至少需要加多少条边可以达到目的
    int f[1 << n];
    memset(f, 0x3f, sizeof f);
    for (int i = 1; i < 1 << n; i++) {
      // 求一下子集i所有点的度数总和和总点数
      int sum = 0, cnt = 0;
      for (int j = 0; j < n; j++)
        if (1 << j & i) sum += a[j], cnt++;
	  // 如果该子集所有点总度数不为0,那么该子集是无法加边达到目的的。否则的话,枚举i的子集
      if (!sum) {
        // 首先cnt - 1条边一定能达到目的
        f[i] = cnt - 1;
        // 枚举i的子集j
        for (int j = 1; j < i; j++)
          if ((i & j) == j) f[i] = min(f[i], f[j] + f[i - j]);
      }
    }

    return f[(1 << n) - 1];
  }
};

时间复杂度 O ( 4 n ) O(4^n) O(4n)(实际不会这么慢,因为和为 0 0 0的子集数一般不会特别多,可以近似认为是 O ( n 2 n ) O(n2^n) O(n2n)复杂度),空间 O ( 2 n ) O(2^n) O(2n)

也可以DFS暴搜来做。只考虑度不为 0 0 0的点,因为平账的过程每一步的顺序是无所谓的,所以可以规定一种顺序来枚举。遍历度不为 0 0 0的点,直接枚举当前的点和其余点里哪一个点去连边,然后暴搜所有可能。注意到对任何最优解,我们总能把这个解调整为按顺序每个点只和之后的某一个点进行平账的过程,从而这样的暴搜是枚举了所有情况的。代码如下:

class Solution {
 public:
  int minTransfers(vector<vector<int>>& ts) {
    unordered_map<int, int> mp;
    for (auto& t : ts) mp[t[0]] -= t[2], mp[t[1]] += t[2];
    vector<int> ds;
    for (auto& [k, v] : mp)
      if (v) ds.push_back(v);
    return dfs(0, ds);
  }

  int dfs(int u, vector<int>& ds) {
    while (u < ds.size() && !ds[u]) u++;
    if (u == ds.size()) return 0;
    int res = 2e9;
    for (int i = u + 1; i < ds.size(); i++)
      if (ds[u] * ds[i] < 0) {
        ds[i] += ds[u];
        res = min(res, 1 + dfs(u + 1, ds));
        ds[i] -= ds[u];
      }
    return res;
  }
};

时间复杂度指数级,空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值