[Kick Start] 2021 Round B

sinkinben發表於2021-07-10

題目:Kick Start 2021 Round-B .

Increasing Substring

輸出字串中每個字元的最長 Increasing Substring 的長度,非常簡單的動態規劃問題。

定義 dp[i] 是以 str[i] 結尾的最長 Increasing Substring 的長度。

轉移方程

dp[i] = dp[i-1] + 1, if str[i-1] < str[i]
dp[i] = 1, otherwise

顯然是可以進行空間優化的,然而「可以但沒必要」。

程式碼實現

#include <iostream>
#include <string>
#include <vector>
using namespace std;
int cnt = 1;
void solve(string &str, int n)
{
    vector<int> dp(n, 1);
    for (int i = 1; i < n; i++)
    {
        if (str[i - 1] < str[i])
            dp[i] = dp[i - 1] + 1;
    }
    printf("Case #%d:", cnt++);
    for (int x : dp) printf(" %d", x);
    printf("\n");
}
int main()
{
    int t, n;
    cin >> t;
    cin.ignore();
    while (t--)
    {
        string str;
        cin >> n;   cin.ignore();
        cin >> str; cin.ignore();
        solve(str, n);
    }
}

Longest Progression

給定一個陣列 \(A[n]\) ,在允許改動一個元素的條件下,找到最長的等差數列的長度(這個數列在陣列中必須是連續的)。

令:

  • left[i] 表示從位置 i 向左延伸,能夠得到的最長等差數列的長度(包含 a[i] );
  • right[i] 表示從位置 i 向右延伸,能夠得到的最長等差數列的長度(包含 a[i] )。

顯然,如果我們允許改動一個位置,那麼掃描陣列中的任意一個數 \(a_i\) ,判斷改動 \(a_i\) 是否能組合得到一個更長的等差數列:

  • 組合 left[i-1]right[i+1]
    • 條件為:\(a_{i+2} - a_{i+1} = a_{i-1} - a_{i-2} \text{ and } a_{i+1} - a_{i-1} = 2(a_{i-1} - a_{i-2})\)
  • 組合 left[i-1]a[i], a[i+1]
    • 條件為:\(a_{i+1} - a_{i-1} = 2(a_{i-1} - a_{i-2})\)
  • 組合 right[i+1]a[i], a[i-1]
    • 條件為:\(a_{i+1} - a_{i-1} = 2(a_{i+2} - a_{i+1})\)
  • 組合 left[i-1]a[i] ,或者組合 right[i+1]a[i] ,二者是必然能實現的,無需任何條件。

程式碼實現

#include <iostream>
#include <vector>
using namespace std;
int cnt = 1;
int solve(int n, vector<int> &a)
{
    if (n <= 3) return n;
    vector<int> left(n, 2), right(n, 2);
    left[0] = right[n - 1] = 1;
    for (int i = 2; i < n; i++)
        if ((a[i] - a[i - 1]) == (a[i - 1] - a[i - 2]))
            left[i] = left[i - 1] + 1;
    for (int i = n - 3; i >= 0; i--)
        if ((a[i + 2] - a[i + 1]) == (a[i + 1] - a[i]))
            right[i] = right[i + 1] + 1;
    int ans = max(left[n - 2] + 1, right[1] + 1);
    for (int i = 1; i < n - 1; i++)
    {
        // left[i-1] + 1 其實就是組合 left[i-1] 和 a[i], 因為允許改動 a[i], 所以這是必然能實現的
        // right[i+1] 與之同理
        ans = max(ans, max(left[i - 1] + 1, right[i + 1] + 1));
        if (i >= 2 && a[i + 1] - a[i - 1] == 2 * (a[i - 1] - a[i - 2]))
            ans = max(ans, left[i - 1] + 2);
        if (i + 2 < n && a[i + 1] - a[i - 1] == 2 * (a[i + 2] - a[i + 1]))
            ans = max(ans, right[i + 1] + 2);
        if (i >= 2 && i + 2 < n &&
            a[i + 1] - a[i - 1] == 2 * (a[i - 1] - a[i - 2]) &&
            a[i - 1] - a[i - 2] == a[i + 2] - a[i + 1])
            ans = max(ans, left[i - 1] + right[i + 1] + 1);
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(0);
    int t, n;
    cin >> t;
    while (t--)
    {
        cin >> n;
        vector<int> nums(n);
        for (int i = 0; i < n; i++) cin >> nums[i];
        printf("Case #%d: %d\n", cnt++, solve(n, nums));
    }
}

Consecutive Primes

給定一個整數 \(n\) ,求兩個相鄰的素數 \(l, r\) ( \(l \cdot r \le n\) ) ,且使得 \(l \cdot r\) 的乘積最大,輸出這個最大乘積。

思路

  • \(k = \sqrt{n}\) , 求出 \(k\) 左側的最大素數為 \(l\)\(k\) 右側的最小素數為 \(r\)
  • 如果 \(l \cdot r \le n\) ,那麼返回 \(l \cdot r\)
  • 否則,存在 \(l_2 < l\)\(l_2\) 是小於 \(l\) 的最大素數,返回 \(l_2 \cdot l\)

正確性證明

  • 最理想的情況是 \(k = \sqrt{n}\) 為一個整數,那麼 \(l = r = \sqrt{n}\) 可以得到最大乘積 \(n\) 。但題目要求為 2 個相鄰的不同素數。
  • 因此,這 2 個素數必然是下面 2 種情況之一(否則不能保證 \(l \cdot r \le n\) ):
    • 一個在 \(k\) 的左側,一個在 \(k\) 的右側。
    • 兩個都在 \(k\) 的左側。
  • 顯然,如果「一左一右」的情況存在,它的乘積必然大於「均在左側」這個乘積,因為 \(l_2 < l < r\)

時間複雜度

  • 素數判定可以在 \(O(\sqrt{k})\) 內完成。
  • 根據 Prime Gap ,兩個相鄰素數 \(p_{i}, p_{i+1}\) 之差可以記為 \(g_i\) .
  • 最壞情況下,我們需要找到 3 個相鄰的素數(需要掃描 2 個 Prime Gap),因此演算法複雜度為 \(O((g_l + g_r) \cdot \sqrt{k})\)\(k = \sqrt{n}\) .
  • 題目給定 \(n \le 10^{18}\) ,因此 \(k \le 10^9\) 。查表得 \(g_l, g_r\) 在 282 - 288 之間,因而這一演算法複雜度是可以接受的。

程式碼實現

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int cnt = 1;
bool isprime(uint64_t k)
{
    for (uint64_t i = 2; i * i <= k; i++)
        if (k % i == 0)  return false;
    return true;
}
uint64_t solve(uint64_t n)
{
    uint64_t k = sqrt(n);
    uint64_t l = k, r = k + 1;
    while (!isprime(l)) l--;
    while (!isprime(r)) r++;
    if (l * r <= n) return l * r;
    uint64_t l2 = l - 1;
    while (!isprime(l2)) l2--;
    return l * l2;
}
int main()
{
    int t;
    cin >> t;
    cin.ignore();
    while (t--)
    {
        uint64_t n;
        cin >> n;
        cin.ignore();
        printf("Case #%d: %llu\n", cnt++, solve(n));
    }
}

Truck Delivery

給定一個樹 \(G\) ,每個頂點 \(1-n\) 代表一個城市,每個邊代表一個公路,公路有 2 個引數 (limit, amount) 。如果經過這一公路的卡車,它的 weight 大於等於 limit ,那麼需要收費 amount ,否則不收費。

問:給定一個 \(Q\) ,表示工作的天數,每一天有 2 個引數 \(Q_i = (C, W)\) ,表示從城市 \(C\) 出發,目的地是城市 \(1\) ,顯然這樣的路徑是唯一的。卡車的 weight\(W\) ,那麼從 \(C \rightarrow 1\) 的這一路徑上,每個公路都對應一個收費。對於每個 \(Q_i\),求這一天中,所有收費的最大公因子。

BFS/DFS

最簡單,也是最暴力的解法。

思路

  • 建圖完成後,執行 bfs(1) ,從城市 \(1\) 開始 BFS,找到 \(1\) 到其他城市 \(2-n\) 的所有路徑。路徑通過一個陣列 pre 記錄,pre[x] 表示 x 的前驅城市。

  • 對於每一個 \(Q_i = (C,W)\) ,找到從 \(C \rightarrow 1\) 的路徑,並找到所有 amount 的最大公因子。

  • 時間複雜度為 \(O(N + Q(N + \log{A}))\) , \(A\)amount 的最大值。

  • 顯然,這個複雜度對於 Test Case 2 來說是不可接受的。

  • 第一步的 BFS 也可以換成 DFS ,因為在樹中,只要遍歷一次,即可找到 \(1\)\(2-n\) 的路徑。

程式碼實現

? 被資料範圍搞死了,amount 的範圍是 \(10^{18}\) ,因此必須用 uint64_t ,我改了資料範圍,但忘了改 ans 的型別;改了 ans 的型別,但忘了改 gcd 的引數;改了 gcd 的引數,但忘了改 gcd 的返回值。最後逼得我 Ctrl+F 把 int 全部替換為 uint64_t

#include <iostream>
#include <vector>
#include <unordered_map>
#include <queue>
using namespace std;
// 'first' is load-limit, 'second' is amount
typedef pair<uint64_t, uint64_t> node_t;
uint64_t cnt = 1;
unordered_map<uint64_t, unordered_map<uint64_t, node_t>> graph;
uint64_t gcd(uint64_t a, uint64_t b) { return b == 0 ? a : gcd(b, a % b); }
vector<uint64_t> bfs(uint64_t n, uint64_t start)
{
    vector<uint64_t> vis(n + 1, false), pre(n + 1, 0);
    queue<uint64_t> q;
    vis[start] = true, q.push(start);
    while (!q.empty())
    {
        uint64_t vex = q.front();
        q.pop();
        for (auto &p : graph[vex])
        {
            uint64_t adjacent = p.first;
            if (!vis[adjacent])
                vis[adjacent] = true, q.push(adjacent), pre[adjacent] = vex;
        }
    }
    return move(pre);
}

int main()
{
    ios::sync_with_stdio(0);
    uint64_t t, n, q;
    uint64_t x, y, limit, amount, city, weight;
    cin >> t;
    while (t--)
    {
        cin >> n >> q;
        graph.clear();
        for (uint64_t i = 0; i < n - 1; i++)
        {
            cin >> x >> y >> limit >> amount;
            graph[x][y] = graph[y][x] = {limit, amount};
        }
        auto pre = bfs(n, 1);
        cout << "Case #" << cnt++ << ": ";
        while (q--)
        {
            cin >> city >> weight;
            uint64_t cur = city, ans = 0;
            while (cur != 1)
            {
                uint64_t prev = pre[cur];
                auto [limit, amount] = graph[prev][cur];
                if (weight >= limit) ans = gcd(amount, ans);
                cur = prev;
            }
            cout << ans << " ";
        }
        cout << '\n';
    }
}

Segement Tree

官方題解使用了線段樹 (Segement Tree) ,但我不會這個資料結構 ? , TO BE DONE.

相關文章