1. 程式人生 > 實用技巧 >【2020牛客多校】2020牛客暑期多校訓練營(第二場)E-Two Matchings——複雜思維與簡單dp

【2020牛客多校】2020牛客暑期多校訓練營(第二場)E-Two Matchings——複雜思維與簡單dp

比賽期間寫博文,隊友我家挖祖墳
主要是我數論只會gcd,所以我只好掛機了

題目連線

注意本文中的部分字母和原文稍有不同,請注意!

題意

定義序列 \(a\) ,滿足如下要求

  • 長度為 \(n\) 的序列 \(a\)\(1, 2, 3... n\) 組成
  • \(a_{a_i} = i\)
  • \(a_i \neq i\)

定義一個字串的費用為\(\sum_{i=1}^{n}w_i - w_{a_i} / 2\)\(w\) 為給出的權值陣列

求兩個滿足上述對序列 \(a\) 的描述的序列 \(p, q\),同時還要滿足 \(p_i \neq q_i\) 對於每一個 \(i\) 都成立

則這兩個序列的費用和的最小值是多少

分析

根據條件

  • 長度為 \(n\) 的序列 \(a\)\(1, 2, 3... n\) 組成
  • \(a_{a_i} = i\)
  • \(a_i \neq i\)

可以得到序列是由基礎序列 \(1, 2, 3...n\) 通過進行兩兩對調得到,且每個值進行且只進行一次對調。(這裡就不仔細證明了,應該……在打這個比賽的人應該都能理解吧)

注意,接下來的討論僅討論排序後的下標,即如果寫著 \(1\) 則指代 sort 後的陣列 \(w\) 中最小的值

最小值

首先是最小的值,那很明顯,把 w 陣列排序後,間隔著相減就可以得到,例如下面已經排序後的下標序列:

\[1, 2, 3, 4, 5, 6 \]

我們可以得到其最小的解為

\[(2 - 1) + (4 - 3) + (6 - 5) \]

我們暫時不去處理這個解,保留原樣

次小值

接下來是次優解,首先應當保證其每一位的值不相同

由於我們已經將最小值的組合取完了,則次優解就有了非常多的限制

我們可以“旋轉”這個數列得到

\[2, 3, 4, 5, 6, 1\rightarrow (3 - 2) + (5 - 4) + (6 - 1) \]

把這個“旋轉”暫時稱為 \(6-rotation\),指代 \(6\) 個元素的旋轉

而此時即為次優的解(之一)。

證明

我們以六個數字的數列來證明上述操作

首先用 \(-\) 表示這個值作為其所在的交換中的較小值, \(+\)

表示這值作為其所在的交換中的較大值

例如最小值可以表示為

1 2 3 4 5 6
- + - + - +

我們並不需要具體考慮哪個值與哪個值交換,因為最終的求和結果是一樣的,即上面的值與下面的符號結合後相加就是最終結果。

除去最小解後,我們只有以下兩種組合方法

下標 1 2 3 4 5 6
最小值 - + - + - +
方案1 - - + - + +
方案2 - - - + + +
錯誤方案 - - + + - +

這裡舉例一個錯誤的方案,雖然看起來此方案是與最小值方案不同,但是注意一下最後兩個值,無論這個錯誤方案怎麼組合,\(5-6\) 必然要發生組合併發生交換,則與最小值的方案出現重複,則不行。

那麼我們比較一下這兩個方案哪個更優

\[\frac{方案1}{方案2} = \frac{abs(-1-2+3-4+5+6)}{abs(-1-2-3+4+5+6)} = \frac{abs(+3-4)}{abs(-3+4)} = \frac{4-3}{4-3} = \frac{1}{1} \]

很明顯,其實哪個方案都是次優的………………這也就是為什麼上面寫著次優的解(之一)的原因。

合併最小值和次小值

我們將兩個解相加發現最終結果為

\[[(2 - 1) + (4 - 3) + (6 - 5)] + [(3 - 2) + (5 - 4) + (6 - 1)] =2 * (6 - 1) \]

長度不及 \(6\) 的時候

而對於長度僅為 \(4\) 的串,只能 \(4-rotation\) ,即

\[1, 2, 3, 4 \rightarrow (4-rotation)\rightarrow 2, 3, 4, 1 \rightarrow (3 - 2) + (4 - 1) \]

此時的最終結果為(過程忽略)

\[2 * (4 - 1) \]

長度為\(8\)的時候

那麼我們再往長度增長的方向考慮,當 \(n = 8\) 時,我們有兩個方案,

  1. 兩個 \(4-rotation\)\(1234\)\(5678\) )來旋轉它
  2. 兩個 \(4-rotation\)\(1278\)\(3456\) )來旋轉它
  3. 一個 \(8-rotation\) 來旋轉它

注意,此題是不存在 \(2-rotation\) 的,因為這毫無意義,所以 \(n = 8\) 時,沒有一個 \(6-rotation\) 和一個 \(2-rotation\) 這樣的組合。

先比較一下兩個 \(4-rotation\)

\[\frac{方案1}{方案2} = \frac{2 * [(4 - 1) + (8 - 5)]}{2 * [(8 - 1) + (6 - 3)]} = \frac{12}{20} \]

我們選擇使用方案 \(1\)

接下來是方案 \(1\) 和方案 \(3\) 的比較

\[\frac{方案1}{方案3} = \frac{2 * [(4 - 1) + (8 - 5)]}{2 * [(8 - 1)]} = \frac{12}{14} \]

此時證明得到方案 \(1\) 在三個方案內最優,此時 \(n =8\) 時的答案為:

\[2 * [(4 - 1) + (8 - 5)] \]

同時我們得到了一個結論:僅存在 \(4-rotation\)\(6-rotation\) 兩種旋轉,如果存在 \(8-rotation\) 則可以將此 \(8-rotation\) 分解為兩個 \(4-rotation\) 可以更優。

長度更長的時候

\(n \geq 10\) 時,即可以將整個串分解成多個 \(4-rotation\) 和多個 \(6-rotation\) 組成。

那麼得到了 \(dp\) 的遞推公式:dp[i] = min(dp[i - 4] + v[i - 1] - v[i - 4], dp[i - 6] + v[i - 1] - v[i - 6])

注意 \(dp\) 的初始值有三個:\(n = 4, n = 6, n = 8 \space (防止n = 8的時候出現2-rotation)\)

AC code

#include <bits/stdc++.h>

using namespace std;

long long dp[200100];

void solve() {
    int T;
    cin >> T;
    for (int ts = 0; ts < T; ++ts) {
        int n;
        cin >> n;
        vector<long long> v;
        for (int i = 0; i < n; ++i) {
            long long tmp;
            cin >> tmp;
            v.push_back(tmp);
        }
        sort(v.begin(), v.end());

        dp[0] = 0;
        dp[4] = v[3] - v[0];
        dp[6] = v[5] - v[0];
        dp[8] = v[7] - v[4] + dp[4];
        for (int i = 10; i <= n; i += 2)
            dp[i] = min(dp[i - 4] + v[i - 1] - v[i - 4], dp[i - 6] + v[i - 1] - v[i - 6]);
        cout << dp[n] * 2 << endl;
    }
}

signed main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
#ifdef ACM_LOCAL
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
    int test_index_for_debug = 1;
    char acm_local_for_debug;
    while (cin >> acm_local_for_debug) {
        if (acm_local_for_debug == '$') exit(0);
        cin.putback(acm_local_for_debug);
        if (test_index_for_debug > 20) {
            throw runtime_error("Check the stdin!!!");
        }
        auto start_clock_for_debug = clock();
        solve();
        auto end_clock_for_debug = clock();
        cout << "Test " << test_index_for_debug << " successful" << endl;
        cerr << "Test " << test_index_for_debug++ << " Run Time: "
             << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl;
        cout << "--------------------------------------------------" << endl;
    }
#else
    solve();
#endif
    return 0;
}