USACO 2022 December Contest,Gold题解

Prob1

(Analysis by Timothy Feng)

Define dp[i][j][k]dp[i][j][k] to be the maximum amount of popularity Bessie can achieve with her friends 1…i1…i, jj moonies, and kk ice cream cones.

  • If Bessie does not want to bribe cow ii, then we can update dp[i+1][j][k]=dp[i][j][k]dp[i+1][j][k]=dp[i][j][k].
  • If Bessie chooses to bribe cow ii, she can optionally spend some ice cream cones to decrease her cost. Loop through 0…k0…k to brute force how many ice cream cones Bessie will spend on cow ii. If Bessie chooses to spend cc cones, then Bessie needs to spend Ci−⌊cXi⌋Ci−⌊cXi⌋ moonies. Therefore, dp[i+1][j−(Ci−⌊cXi⌋)][k−c]=dp[i][j][k]dp[i+1][j−(Ci−⌊cXi⌋)][k−c]=dp[i][j][k].

However, this code runs in O(NAB2)O(NAB2) time.

To do better, suppose that we already know the set of cows that we plan to take. How do we check that inviting these cows is within our budget? We can do this greedily. Start by not spending any cones at all, and spending only money to invite these cows. This might cost more money than we have. Next, we will try to spend some ice cream cones to reduce the amount of money we need to spend. Note that at this point, we would always choose the cow with the smallest XiXi to decrease the total cost most efficiently. In other words, the cows that we bribe with cones is a prefix of all cows when sorted by XiXi. This observation leads us to the fact that for each jj and kk, to choose a new cow ii, we only have one transition to consider. Sort Bessie's friends by increasing XiXi. Note that if we take cow ii, we want to spend all our ice cream cones first before we move on to spending money, so we would use c=min(k,Ci⋅Xi)c=min(k,Ci⋅Xi) cones and Ci−⌊cXi⌋Ci−⌊cXi⌋ moonies. Due to the O(NAB)O(NAB) states we have, this results in an O(NAB)O(NAB) time dp.

We can further remove one dimension. By the same observation from before - that cones are used to the maximum before moonies - for all dp states, either kk equals zero or jj equals AA. For each ii, we now only consider O(A+B)O(A+B) states, leading us to our final O(N(A+B))O(N(A+B)) solution.

定义 dp[i][j][k]dp[i][j][k] 为贝茜与她的 1…i1…i 个朋友、jj 个月之神和 kk 个冰淇淋获得的最大人气值。

  • 如果贝茜不想收买第 ii 头奶牛,那么我们可以更新 dp[i+1][j][k]=dp[i][j][k]dp[i+1][j][k]=dp[i][j][k]。
  • 如果贝茜选择行贿第 ii 头奶牛,她可以选择花费一些冰淇淋来降低成本。循环遍历 0…k0…k 来暴力计算贝茜将在第 ii 头奶牛身上花费多少冰淇淋。如果贝茜选择花费 cc 个冰淇淋,则她需要花费 Ci−⌊cXi⌋Ci−⌊cXi⌋ 个月之神进行收买。因此,dp[i+1][j−(Ci−⌊cXi⌋)][k−c]=dp[i][j][k]dp[i+1][j−(Ci−⌊cXi⌋)][k−c]=dp[i][j][k]。

然而,这份代码的时间复杂度为 O(NAB2)O(NAB2)。

为了更好地解决问题,假设我们已经知道要考虑的奶牛集合。我们如何检查邀请这些奶牛是否在我们的预算内?我们可以贪心地解决这个问题,从不花费任何冰淇淋开始,只花费金钱邀请这些奶牛,这可能会花费更多金钱。接下来,我们将尝试花费一些冰淇淋来减少我们需要花费的金钱。注意,在这个阶段,我们总是选择 XiXi 最小的奶牛来最有效地降低总成本。换句话说,通过 XiXi 排序之后,使用冰淇淋贿赂的奶牛是所有奶牛的前缀。这个观察让我们得出一个结论:对于每个 jj 和 kk,选择一个新的奶牛 ii,我们只有一个转移需要考虑。按 XiXi 递增排序贝茜的朋友。注意,如果我们选择奶牛 ii,我们会先花光所有的冰淇淋,然后再花费金钱,所以我们会使用 c=min(k,Ci⋅Xi)c=min(k,Ci⋅Xi) 份冰淇淋和 Ci−⌊cXi⌋Ci−⌊cXi⌋ 月之神的费用。由于我们拥有 O(NAB)O(NAB) 种状态,因此这导致了一个 O(NAB)O(NAB) 的时间复杂度。

我们可以进一步去掉一个维度。根据之前的观察,冰淇淋在花尽之前被最大限度地使用,因此对于所有的dp状态,kk 要么等于 00,要么 jj 等于 AA。对于每个 ii,我们现在只考虑 O(A+B)O(A+B) 个状态,这使我们得出最终的 O(N(A+B))O(N(A+B)) 解。

Timothy's C++ code:

#include <bits/stdc++.h>
using namespace std;

const int N = 2000 + 1;

int dp[N][2 * N];

void set_max(int &a, int b) {
    if (b > a) a = b;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, moonie, cones;
    cin >> n >> moonie >> cones;

    vector<array<int, 3>> cows(n);
    for (auto &[x, p, c] : cows) {
        cin >> p >> c >> x;
    }
    sort(cows.begin(), cows.end());

    memset(dp, -1, sizeof dp);

    dp[0][moonie + cones] = 0;
    for (int i = 0; i < n; ++i) {
        auto [x, p, c] = cows[i];
        for (int j = 0; j <= moonie + cones; ++j) {
            if (dp[i][j] == -1) continue;

            set_max(dp[i + 1][j], dp[i][j]);
            if (j - c * x >= moonie) {
                set_max(dp[i + 1][j - c * x], dp[i][j] + p);
            } else if (j > moonie) {
                int cost_left = c - (j - moonie) / x;
                if (moonie - cost_left >= 0)
                    set_max(dp[i + 1][moonie - cost_left], dp[i][j] + p);
            } else if (j <= moonie && j - c >= 0) {
                set_max(dp[i + 1][j - c], dp[i][j] + p);
            }
        }
    }

    cout << *max_element(dp[n], dp[n] + moonie + cones + 1) << "n";
}

Nick Wu's Python code:

n, a, b = (int(x) for x in input().split())
dpmoney = [0] * (a+1)
dpcones = [0] * (b+1)
v = sorted([[int(x) for x in input().split()] for _ in range(n)], key = lambda x: x[2])
for p, c, x in v:
  for i in range(a-c+1):
    dpmoney[i] = max(dpmoney[i], dpmoney[i+c] + p)
  for i in range(max(0, a-c+1), min(a+1, a-c+1 + (b // x))):
    conesneed = (i-(a-c)) * x
    dpmoney[i] = max(dpmoney[i], dpcones[conesneed] + p)
  for i in range(b-x*c+1):
    dpcones[i] = max(dpcones[i], dpcones[i+x*c] + p)
    dpmoney[a] = max(dpmoney[a], dpcones[i])
print(dpmoney[0])

Prob2

(Analysis by Joe Li, Larry Xing, Benjamin Qi)

We first present a naive solution.

Let's fix the mountain ii that Bessie is standing on, and consider which mountains she can see. If she can see mountain j>ij>i, that means that for all i<k<ji<k<j, hk−hik−i≤hj−hij−ihk−hik−i≤hj−hij−i. Thus, for each ii, we can calculate how many jj satisfy this property by sweeping from left to right. We repeat this process after every update, yielding a time complexity of O(QN2)O(QN2).

然后我们介绍一种朴素的解法。

首先我们固定贝茜站在哪座山上,考虑她能看到哪些山。如果她能看到 j>ij>i 的山,那么对于所有 i<k<ji<k<j,hk−hik−i≤hj−hij−ihk−hik−i≤hj−hij−i。 因此,对于每个 ii,我们可以通过从左到右扫描来计算有多少个 jj 满足这个条件。我们每次更新后重复这个过程,得到 O(QN2)O(QN2) 的时间复杂度。

However, we can optimize this by precampiong the slopes hj−hij−ihj−hij−i. For each ii, suppose the minimum slope is hji−hiji−ihji−hiji−i. Then, for all j<jij<ji, we know that Bessie cannot see mountain jj. When we update the height of mountain ii, we can update the value of jiji, and also check if any j<jij<ji should now be visible.

This gives us a time complexity of O(QNlogN)O(QNlog⁡N), since updating the slope and maintaining the sorted order requires a binary search (or equivalent data structure). In particular, during each query, we can binary search for jiji in O(logN)O(log⁡N) time, and updates take O(logN)O(log⁡N) time as well.

然而,我们可以通过预处理斜率 hj−hij−ihj−hij−i 来进行优化。 对于每个 ii,假设最小斜率是 hji−hiji−ihji−hiji−i。 那么对于所有 j<jij<ji,我们知道贝茜看不到山 jj。 当我们更新山 ii 的高度时,我们可以更新 jiji 的值,并检查 是否有任何 j<jij<ji 现在可以看到。

这使我们的时间复杂度为 O(QNlogN)O(QNlog⁡N),因为更新斜率并维护排序需要进行二分查找 或等效的数据结构。特别地,在每个查询期间,我们可以在 O(logN)O(log⁡N) 的时间内进行二分查找来寻找 jiji, 并且更新也需要 O(logN)O(log⁡N) 的时间。

Joe's code:

#include <bits/stdc++.h>
using namespace std;

void fastIO() {
    ios_base::sync_with_stdio(false);
    cin.tie(0);
}

typedef long long ll;

int N;
ll h[5100];

int main() {
    fastIO();
    cin >> N;
    for (int i = 1; i <= N; i++) { cin >> h[i]; }
    int Q;
    cin >> Q;
    for (int i = 1; i <= QN; i++) {
        int x, y;
        cin >> x >> y;
        h[x] += y;
        int ans = 0;
        for (int j = 1; j <= N; j++) {
            ll bh = 0, bd = -1;
            for (int k = j + 1; k <= N; k++) {
                if (bd == -1) {
                    ans++;
                    bd = 1, bh = h[k] - h[j];
                } else if ((ll)(h[k] - h[j]) * bd >= (ll)bh * (k - j)) {
                    ans++;
                    bd = k - j, bh = h[k] - h[j];
                }
            }
        }
        cout << ans << "n";
    }
}

To speed this up, we can maintain a "monotonic set" for each index ii. In the iith set, we store in sorted order all indices jj such that for all i<k<ji<k<j, hk−hik−i≤hj−hij−ihk−hik−i≤hj−hij−i. When we perform an update at an index xx, we do the following:

  • For i<xi<x, because the updates always increase the height of a mountain, the value of hx−hix−ihx−hix−i increases. So we may need to insert xx into the monotonic set for ii if it is now visible from ii, and delete any indices greater than xx no longer visible from ii. For each ii, this may be done in O(logN)O(log⁡N) amortized time, for a total of O(NlogN)O(Nlog⁡N) amortized time.
  • For i=xi=x, we can naively reupdate the entire monotonic set for ii, which takes O(N)O(N) or O(NlogN)O(Nlog⁡N) time.
  • For i>xi>x, the update does not affect the monotonic sets.

Thus, we can perform each update in O(NlogN)O(Nlog⁡N). Initially, we perform the process described in the naive solution once to initialize the monotonic sets in O(N2logN)O(N2log⁡N) amortized time. Therefore, the total time complexity is O(N2logN+QNlogN)O(N2log⁡N+QNlog⁡N) or O(N2+QNlogN)O(N2+QNlog⁡N) depending on whether the set you are using supports removing a range of cc consecutive elements in O(c+logN)O(c+log⁡N) time.

Note: to avoid using doubles to compare two fractions abab and cdcd, we can instead compare a⋅da⋅d and b⋅cb⋅c.

然而,我们可以通过预处理斜率 hj−hij−ihj−hij−i 来进行优化。对于每个 ii,假设最小斜率为 hji−hiji−ihji−hiji−i。那么对于所有 $j

为了加速这个过程,我们可以为每个索引 ii 维护一个“单调集合”(monotonic set)。在第 ii 个集合中,我们按顺序存储一些索引 jj,满足对于所有 i<k<ji<k<j 都有 hk−hik−i≤hj−hij−ihk−hik−i≤hj−hij−i。当我们在索引 xx 处执行更新时,我们可以执行以下操作:

  • 对于 $i
  • 对于 i=xi=x,我们可以简单地重新更新整个单调集合,需要 O(N)O(N) 或 O(NlogN)O(Nlog⁡N) 的时间。
  • 对于 i>xi>x,更新不会影响单调集合。

因此,我们可以在 O(NlogN)O(Nlog⁡N) 的时间内完成每次更新。最初,我们可以根据朴素解法的过程一次性初始化单调集合,需要 O(N2logN)O(N2log⁡N) 的平摊时间。因此,总时间复杂度为 O(N2logN+QNlogN)O(N2log⁡N+QNlog⁡N) 或 O(N2+QNlogN)O(N2+QNlog⁡N),具体取决于您所使用的集合是否支持在 O(c+logN)O(c+log⁡N) 时间内删除一段连续的 cc 个元素。

注意:为了避免使用双精度浮点数来比较两个分数 abab 和 cdcd,我们可以比较 a⋅da⋅d 和 b⋅cb⋅c。

Joe's code:

#include <bits/stdc++.h>
using namespace std;

void fastIO() {
    ios_base::sync_with_stdio(false);
    cin.tie(0);
}

typedef long long ll;
#define ff first
#define ss second
int N, Q;
ll h[5100];
set<int> rig[5100]; // monotonic sets

bool comp(int ind, int i1, int i2) {
    // does index i2 to ind have a greater slope than index i1 to ind
    int d1 = abs(ind - i1), d2 = abs(ind - i2);
    ll h1 = h[i1] - h[ind], h2 = h[i2] - h[ind];
    return h2 * d1 >= h1 * d2;
}

int main() {
    fastIO();
    cin >> N;
    for (int i = 1; i <= N; i++) { cin >> h[i]; }
    for (int i = 1; i <= N; i++) {
        for (int j = i + 1; j <= N; j++) {
            if (rig[i].empty()) {
                rig[i].insert(j);
            } else {
                if (comp(i, *rig[i].rbegin(), j)) { rig[i].insert(j); }
            }
        }
    }
    int ans = 0;
    for (int i = 1; i <= N; i++) ans += (int)rig[i].size();
    cin >> Q;
    for (int i = 1; i <= Q; i++) {
        int x, y;
        cin >> x >> y; // update mountain x by incrementing it by height y
        h[x] += y;
        // update the sets to the left of x
        for (int j = 1; j <= x - 1; j++) {
            auto it = rig[j].lower_bound(x);
            bool add = false;
            if ((*it) == x) {
                add = true;
                it++;
            } else {
                --it;
                if (comp(j, (*it), x)) {
                    rig[j].insert(x);
                    ans++;
                    add = true;
                    it++;
                    it++;
                }
            }
            if (add) {
                while (it != rig[j].end() && !comp(j, x, (*it))) {
                    it = rig[j].erase(it);
                    ans--;
                }
            }
        }
        // update the set for x
        ans -= (int)rig[x].size();
        rig[x].clear();
        for (int j = x + 1; j <= N; j++) {
            if (rig[x].empty()) {
                rig[x].insert(j);
                ans++;
            } else {
                if (comp(x, *rig[x].rbegin(), j)) {
                    rig[x].insert(j);
                    ans++;
                }
            }
        }
        cout << ans << "n";
    }
}

Note: The above solution takes nearly 3s to run on some test cases. Depending on your language and implementation, you may have trouble passing all test cases even with the intended time complexity. In particular, a Java analog of the solution above using TreeSets took over 13s on some test cases (slower than the naive solution).

There are several ways to pass this problem without coming too close to the time limit:

  • Use vectors instead of sets (or ArrayLists instead of TreeSets in Java). The time complexity becomes O(N3+QN2)O(N3+QN2) or O(QN2)O(QN2) depending on your implementation, which is worse than the set solution, but we were unable to construct a test case where this solution took more than 2s to run, presumably because erasing from a vector has a good constant factor.
  • Use a segment tree instead of a set. The time complexity is the same as the set solution, but the constant factor is better. Our implementation runs in 0.8s.
  • Use a bitset instead of a set. The time complexity is O(N2+QN2/B)O(N2+QN2/B), where we assume standard operations on B=64B=64-bit integers (for such an integer, checking whether it is nonzero, finding its first set bit, and finding its last set bit) take O(1)O(1) time. The implementation below runs in 0.1s.

注意: 上面的解法在某些测试用例上运行时间接近3秒。根据你使用的语言和实现方式,即使时间复杂度是正确的,你也可能无法通过所有测试用例。特别是,对于使用TreeSets来实现上述解法的Java版本,在某些测试用例上需要13秒以上(比原始解法还慢)。

有几种方式可以通过这道题目,而不会接近时间限制:

  • 使用向量代替集合(或在Java中使用ArrayList代替TreeSet)。时间复杂度变成 O(N3+QN2)O(N3+QN2) 或 O(QN2)O(QN2),具体取决于你的实现方式。该复杂度比集合解法要差一些,但我们无法构造出一些需要超过2秒的测试用例,可能是因为擦除向量元素的常数因子比较好;
  • 使用线段树代替集合。时间复杂度与集合解法相同,但常数因子更好。我们的实现在0.8秒内运行;
  • 使用位集(bitset)代替集合。时间复杂度为 O(N2+QN2/B)O(N2+QN2/B),其中假设在B=64B=64位整数上进行常规操作(对于这样一个整数,查看它是否非零、查找第一个设置位以及查找最后一个设置位所需的时间复杂度都是O(1)O(1))。下面的实现可以在0.1秒内运行。

Ben's code:

#include <bits/stdc++.h>
using namespace std;

using ll = long long;

// Source: https://nyaannyaan.github.io/library/misc/bitset-find-prev.hpp.html
template <size_t Nb> struct Bitset : bitset<Nb> {
    template <typename... Args> Bitset(Args... args) : bitset<Nb>(args...) {}

    static constexpr int N = Nb;
    static constexpr int array_size = (Nb + 63) / 64;

    union raw_cast {
        array<uint64_t, array_size> a;
        Bitset b;
    };

    int _Find_prev(size_t i) const {
        if (i == 0) return -1;
        if ((*this)[--i] == true) return i;
        size_t M = i / 64;
        const auto &a = ((raw_cast *)(this))->a;
        uint64_t buf = a[M] & ((1ull << (i & 63)) - 1);
        if (buf != 0) return M * 64 + 63 - __builtin_clzll(buf);
        while (M--) {
            if (a[M] != 0) return M * 64 + 63 - __builtin_clzll(a[M]);
        }
        return -1;
    }

    inline int _Find_last() const { return _Find_prev(N); }
};

vector<ll> h;
bool comp(int ind, int i1, int i2) {
    return (i1 - ind) * (h[i2] - h[ind]) >= (i2 - ind) * (h[i1] - h[ind]);
}

Bitset<2000> segs[2000];
int N;

void build(Bitset<2000> &b, int st) {
    int lst = st;
    b.reset();
    for (int i = st + 1; i < N; ++i) {
        if (comp(st, lst, i)) {
            lst = i;
            b[i] = 1;
        }
    }
}

int main() {
    cin.tie(0)->sync_with_stdio(0);
    cin >> N;
    h.resize(N);
    for (auto &t : h) cin >> t;
    int ans = 0;
    for (int i = 0; i < N; ++i) {
        build(segs[i], i);
        ans += segs[i].count();
    }
    int Q;
    cin >> Q;
    while (Q--) {
        int x, y;
        cin >> x >> y;
        --x;
        h[x] += y;
        for (int j = 0; j < x; ++j) {
            int it = segs[j]._Find_next(x);
            int it_minus_one = segs[j]._Find_prev(it);
            assert(it_minus_one != -1);
            if (!comp(j, it_minus_one, x)) { continue; }
            if (!segs[j][x]) {
                segs[j][x] = 1;
                ++ans;
            }
            while (it < N) {
                if (comp(j, x, it)) break;
                int next_it = segs[j]._Find_next(it);
                segs[j][it] = 0;
                --ans;
                it = next_it;
            }
        }
        ans -= segs[x].count();
        build(segs[x], x);
        ans += segs[x].count();
        cout << ans << "n";
    }
}

Prob3

(Analysis by Benjamin Qi)

Solution 1:

We can reason as follows.

  • Let vv be some vertex of the graph with the minimum degree. If the optimal friendship group contains vv, then the group is a subset of the connected component of vv. Thus, such a friendship group can have strength at most ss equal to the degree of vv times the size of the connected component of vv. The connected component of vv itself is a friendship group with strength ss as vv has minimum degree, so the highest strength of a friendship group containing vv is ss.
  • If the optimal friendship group doesn't contain vv, we can remove vv from the graph.

We can repeatedly identify the minimum degree vertex vv of the graph, update the answer to be at least ss, and then remove vv in O(M+N)O(M+N) time. However, computing connected components after every vertex removal naively takes O(NM)O(NM) time. We can speed this by reversing the sequence of vertex removals (so that we want to maintain connected components after adding instead of removing a vertex), and then using a Disjoint Set Union data structure. The time complexity is O(Mα(N))O(Mα(N)) (or O(MlogM)O(Mlog⁡M) if a set is used to identity and remove the minimum degree vertex).

题解 1:

我们可以如下推理。

  • 让 vv 为度数最小的顶点。如果最优的友谊团包含 vv,则友谊团是 vv 所在的连通分量的子集。因此,这样的友谊团的强度 ss 最多是 vv 的度数乘以 vv 所在的连通分量的大小。vv 所在的连通分量本身的强度是 ss,因为 vv 有最小度数,所以包含 vv 的友谊团的最高强度是 ss。
  • 如果最优的友谊团不包含 vv,我们可以从图中删除 vv。

我们可以重复识别图的最小度数顶点 vv,更新答案为至少 ss,然后在 O(M+N)O(M+N) 的时间内删除 vv。但是,每次删除顶点后计算连通分量的时间复杂度为 O(NM)O(NM)。我们可以通过反转顶点删除序列(从而希望在添加顶点而不是删除顶点后维护连通分量),然后使用树上启发式合并来加速这一过程。时间复杂度为 O(Mα(N))O(Mα(N))(如果使用集合识别和删除最小度数顶点,则时间复杂度为 O(MlogM)O(Mlog⁡M))。

Timothy Qian's code:

#include <bits/stdc++.h>

using namespace std;

struct DSU {
  vector<int> e;

  DSU(int n) { e = vector<int>(n, -1); }

  int get(int x) { return e[x] < 0 ? x : e[x] = get(e[x]); }

  bool same_set(int a, int b) { return get(a) == get(b); }

  int size(int x) { return -e[get(x)]; }

  bool unite(int x, int y) {
    x = get(x), y = get(y);
    if (x == y) return false;
    if (e[x] > e[y]) swap(x, y);
    e[x] += e[y];
    e[y] = x;
    return true;
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  int n, m;
  cin >> n >> m;
  vector<vector<int>> g(n);
  vector<int> deg(n);
  for (int i = 0; i < m; ++i) {
    int u, v;
    cin >> u >> v;
    --u, --v;
    g[u].push_back(v);
    g[v].push_back(u);
    ++deg[u];
    ++deg[v];
  }
  set<array<int, 2>> vertices;
  for (int i = 0; i < n; ++i) {
    vertices.insert({deg[i], i});
  }
  vector<int> order;
  vector<int> degrees;
  vector<bool> active(n, true);
  auto remove = [&]() {
    auto top = *vertices.begin();
    int u = top[1];
    int degree = top[0];
    order.push_back(u);
    degrees.push_back(degree);
    active[u] = false;
    for (int v : g[u]) {
      if (active[v]) {
        vertices.erase({deg[v], v});
        --deg[v];
        vertices.insert({deg[v], v});
      }
    }
    vertices.erase({deg[u], u});
  };
  for (int i = 0; i < n; ++i) {
    remove();
  }
  reverse(order.begin(), order.end());
  reverse(degrees.begin(), degrees.end());
  active.assign(n, false);
  DSU dsu(n);
  int mx = 1;
  long long ans = 0;
  for (int i = 0; i < n; ++i) {
    int u = order[i];
    active[u] = true;
    for (int v : g[u]) {
      if (active[v]) {
        dsu.unite(u, v);
        mx = max(mx, dsu.size(u));
      }
    }
    ans = max(ans, 1ll * mx * degrees[i]);
  }
  cout << ans << 'n';
  return 0;
}

Solution 2:

Suppose we are looking for the strongest friendship group where the cow with the minimum number of friends has exactly dd friends. We can find such a friendship group as follows: first, repeatedly remove any vertex with degree less than dd from the graph, and then return the largest connected component. We can do this in O(M)O(M) time for each of d=1,2,…d=1,2,…, and so on until the graph is empty. As a friendship group where every member has at least dd friends must contain at least (d+1)d2(d+1)d2 pairs of friendships, so once (d+1)d2>M(d+1)d2>M, the graph must be empty. Thus, this solution runs in O(MM−−√)O(MM) time.

The code solution uses DSU (which adds an extra factor of α(N)α(N)), though this may be substituted with any other method of finding connected components (such as BFS or DFS).

题解 2:

假设我们正在寻找强度最高的友谊团,其中有恰好 dd 个朋友,即最小朋友数的奶牛恰好有 dd 个朋友。我们可以按照以下步骤找到这样的友谊团:首先,反复从图中删除任何度小于 dd 的顶点,然后返回最大的连通分量。我们可以在 d=1,2,…d=1,2,…,以及直到图为空为止的情况下每次都以 O(M)O(M) 的时间完成此操作。由于每个成员都至少有 dd 个朋友的友谊团必须包含至少 (d+1)d2(d+1)d2 对友谊,因此一旦 (d+1)d2>M(d+1)d2>M,则图必须为空。因此,此算法的时间复杂度为 O(MM−−√)O(MM)。

代码解决方案使用并查集(另加一个因素 α(N)α(N)),但这可以替换为查找连通分量的任何其他方法(例如 BFS 或 DFS)。

Nick Wu's code:

#include <algorithm>
#include <cstdio>
#include <numeric>
#include <vector>

using namespace std;

struct disjoint_set {
  vector<int> p, sz;
  disjoint_set(int n) {
    p.assign(n, -1);
    sz.assign(n, 1);
  }
  int find(int x) {
    return p[x] < 0 ? x : (p[x] = find(p[x]));
  }
  int getsz(int x) {
    return sz[find(x)];
  }
  bool merge(int x, int y) {
    x = find(x);
    y = find(y);
    if(x == y) return false;
    p[x] = y;
    sz[y] += sz[x];
    return true;
  }
};

int main() {
  int n, m;
  scanf("%d%d", &n, &m);
  vector<vector<int>> edges(n);
  vector<int> edeg(n);
  for(int i = 0; i < m; i++) {
    int a, b;
    scanf("%d%d", &a, &b);
    a--; b--;
    edeg[a]++;
    edeg[b]++;
    edges[a].push_back(b);
    edges[b].push_back(a);
  }
  int ret = 0;
  vector<bool> deleted(n);
  vector<int> active(n);
  iota(active.begin(), active.end(), 0);
  for(int mindeg = 1; mindeg * mindeg <= m; mindeg++) {
    disjoint_set dsu(n);
    for(int i: active) {
      for(auto j: edges[i]) {
        if(!deleted[j] && dsu.merge(i, j)) ret = max(ret, dsu.getsz(i) * mindeg);
      }
    }
    vector<int> nactive;
    vector<int> q;
    for(int i: active) {
      if(edeg[i] == mindeg) {
        q.push_back(i);
      }
    }
    while(q.size()) {
      int i = q.back(); q.pop_back();
      if(deleted[i]) continue;
      deleted[i] = true;
      for(int j: edges[i]) {
        if(--edeg[j] <= mindeg) {
          q.push_back(j);
        }
      }
      edges[i].clear();
    }
    for(int i: active) if(edeg[i] > mindeg) nactive.push_back(i);
    active.swap(nactive);
  }
  printf("%dn", ret);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值