跳轉到

Aliens 優化

Aliens 優化,利用手續費 w 來限制選的次數,慢慢去逼近選 k 次的 w(可能不存在,但就是去嘗試逼近,因為選的次數還是具有單調性)

Best Time to Buy and Sell Stock

LeetCode 122. Best Time to Buy and Sell Stock II

給你 \(n\) 個股價 \(a_1,\ldots ,a_n\),你可以做最多一次買跟賣,且買跟賣可以在同一天,獲利最大是多少

\(n\le 3\times 10^4,a_i\le 10^4\)

思路

因為可以當天賣又當天買,利潤變成可以拆解的,那我們就 Greedy 的選即可

Image title
Image caption

code
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for (int i = 1; i < prices.size(); i++) {
            result += max(prices[i] - prices[i - 1], 0);
        }
        return result;
    }
};
LeetCode 714. Best Time to Buy and Sell Stock with Transaction Fee

給你 \(n\) 個股價 \(a_1,\ldots ,a_n\),買跟賣不能在同一天,且一次買賣會扣 w 元的手續費,獲利最大是多少

\(k\le n\le 2\times 10^6,a_i\le 10^7\)

思路

dp[i][0/1]: i 之前的最後一張是賣出/還是買進

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + a[i])

dp[i][1] = max(dp[i - 1][0] - a[i] - w, dp[i - 1][1])

AI-666 賺多少

2017 全國賽 AI-666 賺多少

給你 \(n\) 個股價 \(a_1,\ldots ,a_n\),你可以做最多 k 買跟賣,且買跟賣不能在同一天,獲利最大是多少

\(k\le n\le 2\times 10^6,a_i\le 10^7\)

思路

Aliens 優化。在計算的部分,我們可以套入上面手續費的概念,手續費越多,能拿的次數就越小,注意同個手續費能拿的次數可能不只一種,我們這邊一律取最小的次數。

若無 k 的限制,意即我們拿幾次都可以,令此時恰好買賣 t 次,其實意義等同於手續費為 0,同時也是手續費的下限。我們可以將圖畫出來,g(k) 為買 k 次的最大獲利。

Image title

所以我們可以寫出 :

  • 若 k < t,那一定是 k 次最好。

  • 若 k > t,最佳解即是 t 次。

因為在這邊同樣手續費可能對應多種買賣次數,在我們都一律取最小的情況下,我們就是要二分搜第一個「最佳買賣次數」 <= k 的 w。例如 k = 4,那我就會選到 w = 2。

\[ \begin{array}{c|cccccc} w&0&1&2&3&4&5\\ \hline 最佳買賣次數 & 5 & 5 & 3 & 3 & 3 & 1\\ \end{array} \]

我們令「在手續費為 w 的最小買賣次數」為 x。實作上最後要加回去的手續費會是 k * w,不能寫成 x * w,因為你同時可以選擇買賣 k 次或 x 次,那若 x < k(如上圖的 k = 4, x = 3),ans + k*w 顯然是比較大的。

下面的寫法有點不太正統(並未使用小數點二分搜去逼近)

Greedy 作法:(P.60) https://drive.google.com/file/d/1w4Lnxy5OuNN1rJ8nz9nBqakPGhS40g6B/view

code
#pragma GCC optimize("O3,unroll-loops")
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define pb push_back
#define mk make_pair
#define F first
#define S second
#define ALL(x) x.begin(), x.end()

using namespace std;

const int INF = 9e18;
const int maxn = 3e6 + 5;
const int M = 1e9 + 7;

int n, k;
int a[maxn];
int dp[maxn][2];
int cnt[maxn][2];

pii cal(int w) {
    dp[0][1] = -INF;
    cnt[0][1] = 0;
    for (int i = 1; i <= n; i++) {
        dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + a[i]);
        int mn = INF;
        if (dp[i][0] == dp[i - 1][0]) {
            mn = min(cnt[i - 1][0], mn);
        }
        if (dp[i][0] == dp[i - 1][1] + a[i]) {
            mn = min(cnt[i - 1][1], mn);
        }
        cnt[i][0] = mn;

        dp[i][1] = max(dp[i - 1][0] - a[i] - w, dp[i - 1][1]);
        mn = INF;
        if (dp[i][1] == dp[i - 1][0] - a[i] - w) {
            mn = min(cnt[i - 1][0] + 1, mn);
        } 
        if (dp[i][1] == dp[i - 1][1]) {
            mn = min(cnt[i - 1][1], mn);
        }
        cnt[i][1] = mn;
    }
    // total profit, transiction time
    return {dp[n][0], cnt[n][0]};
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> k;

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    auto [p, t] = cal(0);
    if (k > (n / 2) || k >= t) {
        cout << p << '\n';
        return 0;
    }
    int l = 0, r = 100000000;
    while (l < r) {
        int mid = (l + r) / 2;
        if (cal(mid).S <= k) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }

    cout << cal(l).F + k * l << '\n';
} 

CF 125E

K 度限制生成樹 CF 125 E. MST Company

\(n\)\(m\) 邊連通圖,找最小生成樹滿足與點 \(1\) 的度數要恰為 \(k\),印出樹上的邊,或無解

\(n,k\le 5000,m\le 10^5,w_i\le 10^5\)

思路

根據 Aliens 優化,我們將跟點 \(1\) 連接的邊的邊權都加上一個權值 \(w\),二分搜去逼近這個 \(w\),其中 \(w\) 可以正或負且可為小數

二分搜出來的 w,要如何選才能選恰好 k 條邊呢 ? 選滿了與 1 相連的邊到 k 條以後,就不在把與 1 相連的邊加入。

code
#include<bits/stdc++.h>
#define maxn 5500
#define maxm 100100
#define INF (1<<30)
#define PI acos(-1.0)
#define mem(a, b) memset(a, b, sizeof(a))
#define For(i, n) for (int i = 0; i < n; i++)
typedef long long ll;
using namespace std;
int n, m, k, x[maxm], y[maxm], w[maxm], p[maxm], f[maxn];
int cnt, ans[maxn], inx;
double l, r, mid;
bool inline cmp(int i, int j) {
    return (x[i] == 1) * mid + w[i] < (x[j] == 1) * mid + w[j];
}
int findroot(int x) {
    return f[x] = (f[x] == x ? f[x] : findroot(f[x]));
}
void work(bool flag) {
    cnt = inx = 0;
    for (int i = 1; i <= n; i++) f[i] = i;
    sort(p + 1, p + m + 1, cmp);
    for (int i = 1; i <= m; i++) {
        int j = p[i];
        int u = findroot(x[j]), v = findroot(y[j]);
        if (u != v && (cnt + (x[j] == 1) <= k || flag)) {
            f[u] = v;
            ans[inx++] = j;
            if (x[j] == 1) cnt++;
        }
    }
}
int main () {
    scanf("%d%d%d", &n, &m, &k);
    int tot = 0;
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%d", x + i, y + i, w + i);
        p[i] = i;
        if (x[i] > y[i]) swap(x[i], y[i]);
        if (x[i] == 1) tot++;
    }
    //如果根节点的度数小于k,或者结点数大于1,而k == 0 一定不行
    if (tot < k || (n > 1 && k == 0)) {
        puts("-1");
        return 0;
    }
    //看能否生成一棵树
    mid = 0;
    work(1);
    if (inx + 1 < n) {
        puts("-1");
        return 0;
    }
    l = -1e5, r = 1e5;
    while(l + 1e-5 < r && cnt != k) {
        mid = (l + r) / 2;
        work(1);
        if (cnt < k) r = mid;
        else l = mid;
    }
    work(0);
    printf("%d\n", inx);
    for (int i = 0; i < inx - 1; i++) printf("%d ", ans[i]);
    if (inx) printf("%d\n", ans[inx - 1]);
}

/*
5 6 3
3 2 5
4 5 5
1 3 5
1 2 5
1 4 5
1 5 5

*/

相關