牛客練習賽59 E 石子搬運 dp+三分法

有n堆石子,第i堆石子的石子數量是ai{a_{i}}ai​,作爲牛客網的一頭領頭牛,牛牛決定把這些石子搬回牛客。如果牛牛一次搬運的石子數量是k{k}k,那麼這堆石子將對牛牛產生k2{k^{2}}k2的負擔值。牛牛最多隻能搬運m{m}m次,每次搬運可以從一堆石子中選出一些石子搬回牛客,每次搬運不能同時從兩堆石子中選取石子,每次只能搬運整數個石子。牛牛是一隻聰明的牛,他想出了一種搬運計劃可以最小化他搬運完這些石子的負擔值的總和,但是突然牛牛的死敵牛能出現了,牛能每次可以施展以下的魔法:
x v 將第x堆石子的數量變爲v

這打亂了牛牛的計劃,每次牛能施展一次魔法,牛牛就得重新規劃他的搬運方案,但是牛能施展魔法的次數太多了,牛牛根本忙活不過來了,於是他請來了聰明的你幫他寫一個程序計算。

分析一下問題,由題意我們可以知道,每個石塊堆是相互獨立的。也就是每一個石塊堆拿走幾次的最小權值都是固定的,可以算出來的,(就最平均的樣子去拿就可以獲得最小消耗),並且堆與堆之間是獨立的就是可以無視堆的選取順序,我們直接從1-n去考慮也完全可以不影響最終結果。
先來思考如果不改變石堆的數值怎麼算答案。
我們這麼設計dp
f[i][j],前i個堆消耗了j次的最小花費。
g[i][j], 第i個堆消耗j次的最小花費,從上一段可以知道這個g[i][j]是可以預處理出來的。
轉移的話很明顯就是
f[i][j] = min(f[i - 1][k] + g[i][j - k]);
但是這樣子的轉移是一個n^3,如果對於每一個修改我們都暴力做一次 n^3 的dp複雜度就是O(q*n^3),肯定超時。
我們從轉移的過程入手,考慮對於f[i-1][k] 在k從小走到大的時候,由f[i-1][k] 和g[i][j - k]得到的最終權值也就是從大走到小在走到大,那麼這先遞減在遞增的總權值就可以去做一個三分。
這樣的複雜度就是變成了O(q * n ^ 2 * log n)可以通過了。
但是這個三分的思路並不會證明,但是的的確確是過的去的。然後我們對於每一個詢問重新計算一次g[i][j],在暴力跑一次dp就好了。由於複雜度看起來可以過但是蠻危險的所以要儘量有點小優化。卡卡常。就k的枚舉是有範圍的,f[i][j]的j也有範圍的,大於某個數就也沒有繼續做下去的必要。

通過的鏈接在牛客的通過代碼

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<time.h>
#include<string>
#include<cmath>
#include<stack>
#include<map>
#include<set>
#define int long long
//#define double long double
using namespace std;
#define PI  3.1415926535898
#define eqs 1e-6
const long long max_ = 400 + 7;
int mod = 998244353;
const int inf = 1e9;
const long long INF = 1e18;
int read() {
    int s = 0, f = 1;
    char ch = getchar();
    while (ch<'0' || ch>'9') {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (ch >= '0'&&ch <= '9') {
        s = s * 10 + ch - '0';
        ch = getchar();
    }
    return s * f;
}
inline int min(int a, int b) {
    return a < b ? a : b;
}
inline int max(int a, int b) {
    return a > b ? a : b;
}
int f[max_][max_], g[max_][max_], node[max_], n, q, m, sum[max_];
void change(int i) {
    g[i][1] = node[i] * node[i];
    for (int j = 2; j <= min(node[i], m); j++) {
        if (node[i] % j) {
            int num = node[i] % j;
            g[i][j] = (node[i] / j)*(node[i] / j)*(j - num)
                + ((node[i] / j) + 1)* ((node[i] / j) + 1)*num;
        }
        else g[i][j] = (node[i] / j)*(node[i] / j)*j;
    }
}
signed main() {
    n = read(), m = read();
    for (int i = 1; i <= n; i++) {
        node[i] = read();
    }
    for (int i = 1; i <= n; i++) change(i);
    /*for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= min(node[i], m); j++) {
            cout << g[i][j] << " ";
        }
        cout << endl;
    }*/
    q = read();
    while (q--) {
        int a = read(), b = read();
        node[a] = b;
        change(a);
        for (int i = 1; i <= n; i++)sum[i] = sum[i - 1] + node[i];
    //  memset(f, 127, sizeof(f));
        int tt = min(node[1], m - (n - 1));
        for (int i = 1; i <= tt; i++) {
            f[1][i] = g[1][i];
        }
        for (int i = 2; i <= n; i++) {
            tt = min(sum[i], m - (n - i));
            for (int j = i; j <= tt; j++) {
                //前i堆處理完了後搬了j次的最小權值
                //前i-1堆可以搬的次數是[i-1, min( j - 1,sum[i - 1] )];
                //設第i-1堆搬的次數爲A
                //則第i堆搬的次數是j - A;
                int L = i - 1, R = min(j - 1, sum[i - 1]);//A的取值範圍
                while (L < R) {
                    int mid = (L + R) >> 1, t1 = mid + 1;
                    //mid 與 t1去對比
                    int vmid = f[i - 1][mid] + g[i][j - mid],
                        vt1 = f[i - 1][t1] + g[i][j - t1];
                    if (vmid >= vt1)L = mid + 1;//遞減捨棄左邊
                    else R = mid;
                }
                f[i][j] = f[i - 1][R] + g[i][j - R];
            }
        }
        printf("%lld\n", f[n][min(sum[n], m)]);
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章