程式碼源 每日一題 分割 洛谷 P6033合併果子

self_disc發表於2022-04-30

 題目連結:切割 - 題目 - Daimayuan Online Judge

資料加強版連結: [NOIP2004 提高組] 合併果子 加強版 - 洛谷

題目描述

有一個長度為 ∑ai 的木板,需要切割成 n 段,每段木板的長度分別為 a1,a2,…,an。

每次切割,會產生大小為被切割木板長度的開銷。

請你求出將此木板切割成如上 nn 段的最小開銷。

輸入格式

第 1 行一個正整數表示 n。

第 2 行包含 nn 個正整數,即 a1,a2,…,an。

輸出格式

輸出一個正整數,表示最小開銷。

資料範圍

對於全部測試資料,滿足 1≤n,ai≤10^5。

樣例輸入:

5

5 3 4 4 4

樣例輸出:

47 

nlogn解法 

核心思想:貪心

正向考慮題意的話,需要每次將長木板較平均的分割成兩塊,再每次分割出裡面最小的,怎麼才最平均呢?還得找最大值?個人覺得不是那麼好處理,可以考慮下逆向思維,轉換一下題意。 

如何轉換題意呢?將一塊長木板分割為n段,每次的花費為被分割的木板長度,可以等價於被分割成的兩塊合成一塊時,花費為合成的兩塊的長度和,便轉化成了怎樣使它合併成一塊的花費最小問題。(舉個例子,就比如一個長為4的分成一個1一個3,花費為4,跟一個1和一個3合併成一個4,花費為1+3時等價的)

思路:

考慮每次取出兩個最小的合成一個更大的,直到最後只剩一個。

證明:

怎麼證明這個貪心是對的呢?我們可以假設有三個木塊a1<a2<a3,如果取a1,a2合併,需要的花費為(a1+a2)+(a1+a2+a3),如果不取兩個最小的,而取a2,a3,需要花費為(a2+a3)+(a2+a3+a1)顯然比第一種要大。那麼如何推廣到一般情況呢?我們可以這樣想,合併了兩個之後,費用肯定要加上兩個的和,兩個合併成的一個肯定還需要與其他的合併,而用遞迴去想這一部分的花費可以看成是大小固定的,就是說你合併成的還需要去和其他的合併求和,而最終下次合併的和是相同的,那麼讓兩個合併的花費盡量小,花費不就小了嗎?

程式碼實現

怎樣每次找到兩個最小的呢,並加入合併成的那個?我們考慮使用STL自帶的最小堆-優先佇列priority_queue。

複雜度分析:

優先佇列的插入查詢均為logn,複雜度為O(n)*O(logn)即O(nlogn)。

程式碼: 

#include <bits/stdc++.h>
using namespace std;
#define int long long                             //會爆int,所以改為了longlong
priority_queue<int, vector<int>, greater<int>> q; //小根堆
int n, ans;
signed main()
{
    scanf("%lld", &n);
    for (; n--;)
    {
        int x;
        scanf("%lld", &x);
        q.push(x); //初始將n個木塊加入
    }
    while (q.size() >= 2)
    {
        int x1, x2;
        x1 = q.top(), q.pop(); //取出兩次堆頂
        x2 = q.top(), q.pop();
        ans += (x1 + x2);
        q.push(x1 + x2); //加入合併的木塊
    }
    cout << ans << "\n";
}
程式碼源 每日一題 分割 洛谷 P6033合併果子

O(n)解法 

 考慮優化掉每次插入查詢的logn。每次合併成的新的木板肯定是載增大的,也就是說合成的木板是有序的,那麼我們使沒有被合併的那些木板變得有序,每次考慮取兩者隊首元素中較小的,用兩個佇列維護,因為有序所以隊首元素為最小值。對初始佇列的排序考慮桶排。可以在On的時間內完成此題了。(洛谷貌似卡讀入了,所以加了個快讀)

詳見程式碼:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll a[100009]; //記錄大小為i的木板的數量(桶排)
ll ans;
void read(int &x) //優化讀入
{
    int f = 1;
    x = 0;
    char s = getchar();
    while (s < '0' || s > '9')
    {
        if (s == '-')
            f = -1;
        s = getchar();
    }
    while (s >= '0' && s <= '9')
    {
        x = x * 10 + s - '0';
        s = getchar();
    }
    x *= f;
}
int main()
{
    int n;
    read(n);
    for (int i = 1; i <= n; i++)
    {
        int x;
        read(x);
        a[x]++; //大小為x的數量+1
    }
    queue<ll> pre, added; // pre為原始的木板佇列,added為後來合併加入的佇列
    for (int i = 1; i <= 100000; i++)
    {
        while (a[i]--) //因為i可能不止一個
        {
            pre.push(i); //放入佇列中,使得pre是有序的
        }
    }
    for (int i = 1; i <= n - 1; i++) // n個需要合併n-1次
    {
        ll x1, x2;
        if ((!pre.empty() && !added.empty() && pre.front() < added.front()) || added.empty()) // pre的隊首小於added的隊首或者added為空
        {
            x1 = pre.front(); //從pre取
            pre.pop();
        }
        else
        {
            x1 = added.front(); //從added取
            added.pop();
        }
        //重複一次操作取x2
        if ((!pre.empty() && !added.empty() && pre.front() < added.front()) || added.empty()) // pre的隊首小於added的隊首或者added為空
        {
            x2 = pre.front();
            pre.pop();
        }
        else
        {
            x2 = added.front();
            added.pop();
        }
        ans += (x1 + x2);    //加上花費
        added.push(x1 + x2); // added中加入新合成的木板
    }
    cout << ans;
}
程式碼源 每日一題 分割 洛谷 P6033合併果子


相關文章