Codeforces Round 982 div2 個人題解(A~D2)

ExtractStars發表於2024-10-27

Codeforces Round 982 div2 個人題解(A~D2)

Dashboard - Codeforces Round 982 (Div. 2) - Codeforces

火車頭

#define _CRT_SECURE_NO_WARNINGS 1

#include <algorithm>
#include <array>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <list>
#include <map>
#include <numeric>
#include <queue>
#include <random>
#include <set>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>

#define ft first
#define sd second

#define yes cout << "yes\n"
#define no cout << "no\n"

#define Yes cout << "Yes\n"
#define No cout << "No\n"

#define YES cout << "YES\n"
#define NO cout << "NO\n"

#define pb push_back
#define eb emplace_back

#define all(x) x.begin(), x.end()
#define all1(x) x.begin() + 1, x.end()
#define unq_all(x) x.erase(unique(all(x)), x.end())
#define unq_all1(x) x.erase(unique(all1(x)), x.end())
#define sort_all(x) sort(all(x))
#define sort1_all(x) sort(all1(x))
#define reverse_all(x) reverse(all(x))
#define reverse1_all(x) reverse(all1(x))

#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL

#define RED cout << "\033[91m"
#define GREEN cout << "\033[92m"
#define YELLOW cout << "\033[93m"
#define BLUE cout << "\033[94m"
#define MAGENTA cout << "\033[95m"
#define CYAN cout << "\033[96m"
#define RESET cout << "\033[0m"

// 紅色
#define DEBUG1(x)                     \
    RED;                              \
    cout << #x << " : " << x << endl; \
    RESET;

// 綠色
#define DEBUG2(x)                     \
    GREEN;                            \
    cout << #x << " : " << x << endl; \
    RESET;

// 藍色
#define DEBUG3(x)                     \
    BLUE;                             \
    cout << #x << " : " << x << endl; \
    RESET;

// 品紅
#define DEBUG4(x)                     \
    MAGENTA;                          \
    cout << #x << " : " << x << endl; \
    RESET;

// 青色
#define DEBUG5(x)                     \
    CYAN;                             \
    cout << #x << " : " << x << endl; \
    RESET;

// 黃色
#define DEBUG6(x)                     \
    YELLOW;                           \
    cout << #x << " : " << x << endl; \
    RESET;

using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
// typedef __int128_t i128;

typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ld, ld> pdd;
typedef pair<ll, int> pli;
typedef pair<string, string> pss;
typedef pair<string, int> psi;
typedef pair<string, ll> psl;

typedef tuple<int, int, int> ti3;
typedef tuple<ll, ll, ll> tl3;
typedef tuple<ld, ld, ld> tld3;

typedef vector<bool> vb;
typedef vector<int> vi;
typedef vector<ll> vl;
typedef vector<string> vs;
typedef vector<pii> vpii;
typedef vector<pll> vpll;
typedef vector<pli> vpli;
typedef vector<pss> vpss;
typedef vector<ti3> vti3;
typedef vector<tl3> vtl3;
typedef vector<tld3> vtld3;

typedef vector<vi> vvi;
typedef vector<vl> vvl;

typedef queue<int> qi;
typedef queue<ll> ql;
typedef queue<pii> qpii;
typedef queue<pll> qpll;
typedef queue<psi> qpsi;
typedef queue<psl> qpsl;

typedef priority_queue<int> pqi;
typedef priority_queue<ll> pql;
typedef priority_queue<string> pqs;
typedef priority_queue<pii> pqpii;
typedef priority_queue<psi> pqpsi;
typedef priority_queue<pll> pqpll;
typedef priority_queue<psi> pqpsl;

typedef map<int, int> mii;
typedef map<int, bool> mib;
typedef map<ll, ll> mll;
typedef map<ll, bool> mlb;
typedef map<char, int> mci;
typedef map<char, ll> mcl;
typedef map<char, bool> mcb;
typedef map<string, int> msi;
typedef map<string, ll> msl;
typedef map<int, bool> mib;

typedef unordered_map<int, int> umii;
typedef unordered_map<ll, ll> uml;
typedef unordered_map<char, int> umci;
typedef unordered_map<char, ll> umcl;
typedef unordered_map<string, int> umsi;
typedef unordered_map<string, ll> umsl;

std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());

template <typename T>
inline T read()
{
    T x = 0;
    int y = 1;
    char ch = getchar();
    while (ch > '9' || ch < '0')
    {
        if (ch == '-')
            y = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9')
    {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * y;
}

template <typename T>
inline void write(T x)
{
    if (x < 0)
    {
        putchar('-');
        x = -x;
    }
    if (x >= 10)
    {
        write(x / 10);
    }
    putchar(x % 10 + '0');
}

/*#####################################BEGIN#####################################*/
void solve()
{
}

int main()
{
    ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
    // freopen("test.in", "r", stdin);
    // freopen("test.out", "w", stdout);
    int _ = 1;
    std::cin >> _;
    while (_--)
    {
        solve();
    }
    return 0;
}

/*######################################END######################################*/
// 連結:

A. Rectangle Arrangement

您正在為一個無限的正方形網格著色,其中所有單元格最初都是白色的。為此,您將獲得 \(n\) 個印章。每個印章都是一個寬度為 \(w_i\)、高度為 \(h_i\) 的矩形。

您將使用每個印章恰好一次,將與網格上的印章大小相同的矩形塗成黑色。您不能旋轉印章,並且對於每個單元格,印章必須完全覆蓋它或根本不覆蓋它。您可以在網格上的任何位置使用印章,即使印章區域覆蓋的部分或全部單元格已經是黑色。

使用完所有印章後,您可以獲得的黑色方塊連線區域的周長的最小總和是多少?

輸入
每個測試包含多個測試用例。第一行包含測試用例的數量 \(t\) \((1 \leq t \leq 500)\)。測試用例的描述如下。

每個測試用例的第一行包含一個整數 \(n\) \((1 \leq n \leq 100)\)

接下來的 \(n\) 行中的第 \(i\) 行包含兩個整數 \(w_i\)\(h_i\) \((1 \leq w_i, h_i \leq 100)\)

輸出
對於每個測試用例,輸出一個整數——使用完所有郵票後可以獲得的黑色方塊連通區域周長的最小和。

示例
輸入

5
5
1 5
2 4
3 3
4 2
5 1
3
2 2
1 1
1 2
1
3 2
3
100 100
100 100
100 100
4
1 4
2 3
1 5
3 2

輸出

20
8
10
400
16

提示
在第一個測試用例中,印章可以如左側所示使用。每個印章用不同的顏色突出顯示以便於識別。

img

在使用完這些印章後,形成一個黑色區域(如右側所示),其周長為 20。可以證明,沒有其他方式使用印章可以得到更低的總周長。

在第二個測試用例中,第二個和第三個印章可以完全放置在第一個印章內部,因此最小周長為 8。解題思路

解題思路

小學數學,觀察樣例發現總周長一定為$2\times (\text{max}(w)+\text{max}(h)) $

當然觀察資料範圍發現,也可以使用暴力模擬

程式碼實現

方法一

void solve()
{
    int n;
    cin >> n;
    int max_w = 0, max_h = 0;
    for (int i = 0; i < n; i++)
    {
        int w, h;
        cin >> w >> h;
        if (w > max_w)
            max_w = w;
        if (h > max_h)
            max_h = h;
    }
    cout << 2 * (max_w + max_h) << "\n";
}

方法二

賽時寫的方法二,想了下好像會被hack掉,有可能超時,有改成方法一了,結果賽後再交沒超時,白丟分

void solve()
{
    int n;
    cin >> n;
    vvi board(102, vi(102, 0));
    for (int i = 0; i < n; i++)
    {
        int x;
        int y;
        cin >> x >> y;
        // 把所有印章從左上角開始印
        for (int j = 1; j <= x; j++)
        {
            for (int k = 1; k <= y; k++)
            {
                board[j][k] = 1;
            }
        }
    }
    int ans = 0;
    for (int i = 1; i <= 100; i++)
    {
        for (int j = 1; j <= 100; j++)
        {
            if (board[i][j] == 1)
            {
                // 如果遇到一個方塊蓋了章,給答案加4,再減去和相鄰方塊也蓋了章的數量
                ans += 4;
                ans -= board[i - 1][j] + board[i + 1][j] + board[i][j - 1] + board[i][j + 1];
            }
        }
    }
    cout << ans << endl;
}

B. Stalin Sort

斯大林排序是一種有趣的排序演算法,旨在消除不合適的元素,而不是費心對它們進行正確排序,這使其具有 \(O(n)\) 的時間複雜度。

它的流程如下:從陣列中的第二個元素開始,如果它嚴格小於前一個元素(忽略那些已經被刪除的元素),則刪除它。繼續遍歷陣列,直到按非遞減順序排序。例如,陣列 \([1,4,2,3,6,5,5,7,7]\) 經過斯大林排序後變為 \([1,4,6,7,7]\)

如果您可以透過對任何子陣列重複應用斯大林排序,按非遞增順序對陣列進行排序,則我們將陣列定義為易受攻擊的。

給定一個包含 \(n\) 個整數的陣列 \(a\),確定必須從陣列中刪除的最小整數數量,以使其易受攻擊。

輸入
每個測試由多個測試用例組成。第一行包含一個整數 \(t\) \((1 \leq t \leq 500)\) — 測試用例的數量。後面是測試用例的描述。

每個測試用例的第一行包含一個整數 \(n\) \((1 \leq n \leq 2000)\) — 陣列的大小。

每個測試用例的第二行包含 \(n\) 個整數 \(a_1,a_2,…,a_n\) \((1 \leq a_i \leq 10^9)\)

保證所有測試用例的 \(n\) 之和不超過 2000。

輸出
對於每個測試用例,輸出一個整數——必須從陣列中刪除的最小整數數量,以使其易受攻擊。

示例
輸入

6
7
3 6 4 9 2 5 2
5
5 4 4 2 2
8
2 2 4 4 6 6 10 10
1
1000
9
6 8 9 10 12 9 7 5 4
7
300000000 600000000 400000000 900000000 200000000 400000000 200000000

輸出

2
0
6
0
4
2

提示
在第一個測試用例中,最優答案是刪除數字 3 和 9。然後我們剩下 \(a=[6,4,2,5,2]\)。為了證明這個陣列是易受攻擊的,我們可以首先對子陣列 \([4,2,5]\) 應用斯大林排序,得到 \(a=[6,4,5,2]\),然後對子陣列 \([6,4,5]\) 應用斯大林排序,得到 \(a=[6,2]\),該陣列是非遞增的。

在第二個測試用例中,陣列已經是非遞增的,因此我們不需要刪除任何整數。

解題思路

題目要求剩下的陣列按照非遞減排序,所以無論最後陣列是什麼情況,我們一定可以對它進行斯大林排序,使得它只剩一種數字。

因此,題目實際上可以轉化成有兩種操作

  • 花費1代價刪除任意數字
  • 花費0代價刪除\(a_j(a_j \lt a_i,j \gt i)\)

所以我們可以列舉剩下的數字\(a_i\),它的花費\(cost_i=i-1+\sum_{j=i+1}^{n}[a_j\lt a_i]\)

由於\(n\le2000\),我們可以直接列舉統計\(a_j\lt a_i\)的數量

程式碼實現

方法一

void solve()
{
    int n;
    cin >> n;
    vi a(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    int ans = inf;
    for (int i = 1; i <= n; i++)
    {
        int cnt = 0;
        for (int j = i + 1; j <= n; j++)
        {
            if (a[j] > a[i])
                cnt++;
        }
        ans = min(ans, i - 1 + cnt);
    }
    cout << ans << endl;
}

方法二

對於\(n\le200000\),方法一會超時,所有這時候我們可以倒著列舉\(a_i\),然後開一個對頂堆儲存\(a_i\)

void solve()
{
    int n;
    cin >> n;
    vi a(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    int ans = inf;
    priority_queue<int, vi, greater<int>> ge;
    priority_queue<int> le;
    for (int i = n; i >= 1; i--)
    {
        while (!ge.empty() && ge.top() <= a[i])
        {
            le.push(ge.top());
            ge.pop();
        }
        while (!le.empty() && le.top() > a[i])
        {
            ge.push(le.top());
            le.pop();
        }
        ans = min(ans, int(i - 1 + ge.size()));
        ge.push(a[i]);
    }
    cout << ans << endl;
}

C. Add Zeros

給定一個陣列 \(a\),最初包含 \(n\) 個整數。在一次操作中,您必須執行以下操作:

選擇一個位置 \(i\),使得 \(1<i\leq |a|\)\(a_i=|a|+1−i\),其中 \(|a|\) 是陣列的當前大小。
\(i−1\) 個零附加到 \(a\) 的末尾。
在執行此操作多次後,陣列 \(a\) 的最大可能長度是多少?

輸入
每個測試包含多個測試用例。第一行包含測試用例的數量 \(t\) \((1 \leq t \leq 1000)\)。測試用例的描述如下。

每個測試用例的第一行包含 \(n\) \((1 \leq n \leq 3 \cdot 10^5)\)——陣列 \(a\) 的長度。

每個測試用例的第二行包含 \(n\) 個整數 \(a_1,a_2,…,a_n\) \((1 \leq a_i \leq 10^{12})\)

保證所有測試用例的 \(n\) 之和不超過 \(3 \cdot 10^5\)

輸出
對於每個測試用例,輸出一個整數——執行某些操作序列後可能的最大長度為 \(a\)

示例
輸入

4
5
2 4 6 2 5
5
5 4 4 5 1
4
6 8 2 3
1
1

輸出

10
11
10
1

提示
在第一個測試用例中,我們可以首先選擇 \(i=4\),因為 \(a_4=5+1−4=2\)。之後,陣列變為 \([2,4,6,2,5,0,0,0]\)。然後我們可以選擇 \(i=3\),因為 \(a_3=8+1−3=6\)。之後,陣列變為 \([2,4,6,2,5,0,0,0,0,0]\),其長度為 10。可以證明沒有其他操作序列會使最終陣列更長。

在第二個測試用例中,我們可以選擇 \(i=2\),然後 \(i=3\),再然後 \(i=4\)。最終陣列將是 \([5,4,4,5,1,0,0,0,0,0,0]\),長度為 11。

解題思路

我們把題目給的式子轉化一下,變成\(|a|=a_i+i-1\)

由於每個\(a_i\)操作一次會使得\(|a|\)增加\(i-1\),實際上就相當於每個\(a_i\)都是對\(a_i+i-1\)\(a_i+2i-2\)建了一條邊,點權為陣列長度。

所以我們可以按照上面的規則進行建圖,對這個圖從\(n\)進行\(dfs\)\(dfs\)過程中維護一下遇到的最大點權,即為答案。

記得要加\(vis\)陣列,不然會超時。

程式碼實現

void solve()
{
    ll n;
    cin >> n;
    vl a(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    map<ll, vl> adj;
    for (ll i = 1; i <= n; i++)
    {
        adj[a[i] + i - 1].pb(a[i] + 2 * (i - 1));
    }
    ll ans = n;
    mlb vis;
    function<void(ll, ll)> dfs = [&](ll u, ll fa)
    {
        ans = max(ans, u);
        vis[u] = 1;
        for (auto &v : adj[u])
        {
            if (v == fa)
                continue;
            if (vis[v])
                continue;
            dfs(v, u);
        }
    };
    dfs(n, 0);
    cout << ans << endl;
}

D1. The Endspeaker (Easy Version)

這是該問題的簡單版本。唯一的區別是,您只需要在此版本中輸出最小的總操作成本。您必須解決兩個版本才能破解。

您將獲得一個長度為 \(n\) 的陣列 \(a\) 和一個長度為 \(m\) 的陣列 \(b\)(對於所有 \(1 \leq i < m\),其值為 \(b_i > b_{i+1}\))。最初,\(k\) 的值為 1。您的目標是透過重複執行以下兩個操作之一來使陣列 \(a\) 為空:

型別 1 — 如果 \(k\) 的值小於 \(m\) 且陣列 \(a\) 不為空,則可以將 \(k\) 的值增加 1。這不會產生任何成本。
型別 2 — 刪除陣列 \(a\) 的非空字首,使其總和不超過 \(b_k\)。這會產生 \(m−k\) 的成本。

您需要最小化操作的總成本,以使陣列 \(a\) 為空。如果透過任何操作序列都無法做到這一點,則輸出 \(−1\)。否則,輸出操作的最小總成本。

輸入
每個測試包含多個測試用例。第一行包含測試用例的數量 \(t\) \((1 \leq t \leq 1000)\)。測試用例的描述如下。

每個測試用例的第一行包含兩個整數 \(n\)\(m\) \((1 \leq n,m \leq 3 \cdot 10^5, 1 \leq n \cdot m \leq 3 \cdot 10^5)\)

第二行包含 \(n\) 個整數 \(a_1,a_2,…,a_n\) \((1 \leq a_i \leq 10^9)\)

第三行包含 \(m\) 個整數 \(b_1,b_2,…,b_m\) \((1 \leq b_i \leq 10^9)\)

還保證所有 \(1 \leq i < m\) 的結果是 \(b_i > b_{i+1}\)

保證所有測試用例的 \(n \cdot m\) 之和不超過 \(3 \cdot 10^5\)

輸出
對於每個測試用例,如果有可能使 \(a\) 為空,則輸出操作的最小總成本。如果沒有可能的操作序列使 \(a\) 為空,則輸出單個整數 \(−1\)

示例
輸入

5
4 2
9 3 4 3
11 7
1 2
20
19 18
10 2
2 5 2 1 10 3 2 9 9 6
17 9
10 11
2 2 2 2 2 2 2 2 2 2
20 18 16 14 12 10 8 6 4 2 1
1 6
10
32 16 8 4 2 1

輸出

1
-1
2
10
4

提示
在第一個測試用例中,最優操作序列的總成本為 1,過程如下:

  1. 執行型別 2 的操作,選擇字首為 \([9]\),成本為 1。
  2. 執行型別 1 的操作,\(k\) 現在為 2,成本為 0。
  3. 執行型別 2 的操作,選擇字首為 \([3,4]\),成本為 0。
  4. 執行型別 2 的操作,選擇字首為 \([3]\),成本為 0。

陣列 \(a\) 現在為空,所有操作的總成本為 1。

在第二個測試用例中,由於 \(a_1 > b_1\),無法刪除任何字首,因此陣列 \(a\) 不能透過任何操作序列變為空。

解題思路

觀察題目,發現是使用多種操作完成一個目標求最小代價,考慮dp

我們設\(dp[i][j]\)為對於前\(i\)個數字,使用第\(j\)總操作時的最小代價。

易得狀態轉移方程

\[dp[i][j]= \text{min}\{dp[p][k]\}+m-j,\text{sum}[p,i]\le b[j],1\le k\le j,1\le p\le i \]

其中\(\text{sum}[p,i]\)陣列\(a_p \sim a_i\)的區間和

根據狀態轉移方程寫出遞推過程

for (int i = 1; i <= n; i++)
{
    for (int j = 1; j <= m; j++)
    {
        int p = i;
        while (p >= 1 && pre[i] - pre[p - 1] <= b[j])
        {
            p--;
        }
        p++;
        for (int k = 1; k <= j; k++)
        {
            dp[i][j] = min(dp[i][j], dp[p - 1][k] + m - j);
        }
    }
}

發現時間複雜度為\(O(nm\times \text{max}(n,m))\),在\(n\)\(m\)特別大的情況下肯定超時。

考慮最佳化。

最佳化一

對於\(p\)的位置,我們可以使用二分最佳化列舉

function<int(int, ll)> bianrySearch = [&](int x, ll val) -> int
{
    int p = x;
    int l = 1, r = x;
    while (l < r)
    {
        int mid = (l + r) >> 1;
        if (pre[x] - pre[mid - 1] <= val)
        {
            r = mid;
            p = mid;
        }
        else
        {
            l = mid + 1;
        }
    }
    return p;
};

for (int i = 1; i <= n; i++)
{
    for (int j = 1; j <= m; j++)
    {
        int l = 1, r = i;
        int p = bianrySearch(i, b[j]);
        for (int k = 1; k <= j; k++)
        {
            dp[i][j] = min(dp[i][j], dp[p - 1][k] + m - j);
        }
    }
}

時間複雜度降為\(O(nm^2)\),在\(m\)特別大的時候仍會超時

最佳化二

\(k\)的列舉,我們實際上是在查詢區間\([1,j]\)的最小\(dp[i][j]\)值,所以我們可以使用樹狀陣列儲存\(dp[i][j]\)

樹狀陣列實現

template <typename T>
struct Fenwick
{
    int n;
    vector<T> a;
    Fenwick(int n_ = 0) { init(n_); }
    void init(int n_) { n = n_, a.assign(n + 1, T{inf}); }
    void add(int x, const T &v)
    {
        for (int i = x; i <= n; i += i & -i)
            a[i] = min(a[i], v);
    }
    T query(int x)
    {
        T res{inf};
        for (int i = x; i > 0; i -= i & -i)
            res = min(res, a[i]);
        return res;
    }
};

遞推過程

for (int i = 1; i <= n; i++)
{
    for (int j = 1; j <= m; j++)
    {
        if(b[j]<a[i])// 不能刪除時跳過
            continue;
        int p = bianrySearch(i, b[j]);
        ll mn = tr[p - 1].query(j);// 使用樹狀陣列查詢,查詢1<=k<=j中最小的dp[i][k]
        dp[i][j] = min(dp[i][j], mn + m - j);// 更新dp[i][j]
        tr[i].add(j, dp[i][j]);// 將dp[i][j]插入樹狀陣列中,以便後續查詢
    }
}

時間複雜度\(O(nm\times \text{max}(\log n,\log m))\)

這時已經可以透過該題目了,但是還可以進行最佳化

最佳化三

考慮\(j\)只能由\(j-1\)轉移過來,因此我們可以在最外層迴圈列舉\(j\),使用滾動陣列最佳化掉樹狀陣列

for (int j = 1; j <= m; j++)
{
    for (int i = 1; i <= n; i++)
    {
        if (b[j] < a[i])
            continue;
        int p = bianrySearch(i, b[j]);
        dp[i] = min(dp[i], dp[p - 1] + m - j);
    }
}

時間複雜度為\(O(nm\log n)\)

最佳化四

觀察發現,在求\(p\)的過程中,我們將其轉成一個大小為\(b_j\)的視窗在陣列\(a\)上移動,其中左邊界為\(p\),右邊界為\(i\),這樣我們就最佳化掉了二分

for (int j = 1; j <= m; j++)
{
    int l = 0, r = 1;
    while (r <= n)
    {
        while (l < r && pre[r] - pre[l] > b[j])
            l++;
        if (l < r)
            dp[r] = min(dp[r], dp[l] + m - j);
        r++;
    }
}

時間複雜度為\(O(nm)\)

程式碼實現

void solve()
{
    int n, m;
    cin >> n >> m;
    vector<ll> a(n + 1);
    vector<ll> pre(n + 1);
    ll max_a = 0;

    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        max_a = max(max_a, a[i]);
        pre[i] = pre[i - 1] + a[i];
    }

    vl b(m + 1);
    for (int i = 1; i <= m; i++)
    {
        cin >> b[i];
    }
    if (max_a > b[1])
    {
        cout << "-1\n";
        return;
    }
    vector<ll> dp(n + 1, infll);
    dp[0] = 0;
    for (int j = 1; j <= m; j++)
    {
        int l = 0, r = 1;
        while (r <= n)
        {
            while (l < r && pre[r] - pre[l] > b[j])
                l++;
            if (l < r)
                dp[r] = min(dp[r], dp[l] + m - j);
            r++;
        }
    }
    cout << dp[n] << "\n";
}

D2. The Endspeaker (Hard Version)

這是該問題的難版本。唯一的區別是,您還需要在此版本中輸出最佳序列的數量。

解題思路

順著\(\text{D1}\)版本最後最佳化的思路,我們可以維護一個共享右邊界的雙重滑動視窗,滑動視窗大小為\(b[j]\)

\(dp[i]\)為選擇前\(i\)個數字時的最小花費,\(cnt[i]\)為選擇前\(i\)個數字最小花費情況下的運算元量。

\(l\)為第一左邊界,\(L\)為第二左邊界,\(l\)負責維護\(dp[r]\)從哪裡轉移過來,\(L\)負責\(cnt[r]\)從哪裡轉移過來。

對於\(dp[r]\)來說,轉移方程和\(\text{D1}\)一致,對於\(cnt[r]\)來說,\(cnt[r]=\sum_{L=l}^{R,dp[R]=dp[l]}cnt[L]\)

程式碼實現

const ll mod = 1e9 + 7;

void solve()
{
    int n, m;
    cin >> n >> m;
    vector<ll> a(n + 1);
    vector<ll> pre(n + 1);
    ll max_a = 0;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        max_a = max(max_a, a[i]);
        pre[i] = pre[i - 1] + a[i];
    }
    vector<ll> b(m + 1);
    for (int i = 1; i <= m; i++)
    {
        cin >> b[i];
    }
    if (max_a > b[1])
    {
        cout << "-1\n";
        return;
    }

    vector<ll> dp(n + 1, inf);
    dp[0] = 0;
    vector<ll> cnt(n + 1, 0);
    cnt[0] = 1;
    for (int j = 1; j <= m; j++)
    {
        int l = 0, r = 1;
        int L = 0;
        ll sum = 0;
        while (r <= n)
        {
            // 視窗大小大於b[j],視窗內的序列都不可以用,要全部丟棄
            while (l < r && pre[r] - pre[l] > b[j])
            {
                sum = (sum + mod - cnt[l]) % mod;
                l++;
            }
            // 如果第二左邊界小於了第一左邊界,說明指向的右邊界不同了,要將sum重置
            if (L < l)
            {
                L = l;
                sum = 0;
            }
            // 將共同左邊界的操作序列數量全部記錄
            while (L < r && dp[L] == dp[l])
            {
                sum = (sum + cnt[L]) % mod;
                L++;
            }
            if (l < r)
            {
                ll cost = dp[l] + m - j;
                if (cost < dp[r])
                // 如果花費更少,更新dp和cnt
                {
                    dp[r] = cost;
                    cnt[r] = sum;
                }
                else if (cost == dp[r]) // 花費相同,cnt加上sum
                {
                    cnt[r] = (cnt[r] + sum) % mod;
                }
            }
            r++;
        }
    }
    cout << dp[n] << " " << cnt[n] << "\n";
}

賽時把D1 dp的i和j寫反了,找這個bug找了快一個小時,麻了,後面想到正解沒時間寫了,結束後三分鐘寫完了。太菜了,這幾場掉大分。

相關文章