T1:牛奶供应(三)

站在提前生产的角度来思考,为了缩小总成本,如果某天足够便宜就多生产一些牛奶。但这样的想法会遇到一个困难,也就是现在生产牛奶到底要生产多少箱,它是由之后的若干天决定的,而之后每天生产的成本和生产数量都不固定。

可以考虑逆向思维。

假设每天的牛奶都是当天生产出来的,但这样特别贵的日子就会很不划算。现在我们假设我们可以穿越时光回到过去,告诉前几天的我自己我要多生产多少箱从而满足我穿越来的那天所需要的牛奶需求。

一种做法是假设每天都要承担一个 \(i \times s\) 的保存费用,然后前面的最小历史单价就不需要承担保存费用

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)

using std::cin;
using std::cout;
using ll = long long;

int main() {
    ll n, s;
    cin >> n >> s;
    
    ll ans = 0;
    ll mn = 1001001001; // 历史最小有效单价
    rep(i, n) {
        ll c, a;
        cin >> c >> a;
        c -= i*s;
        if (c < mn) mn = c;
        ans += (mn+i*s) * a;
        // std::cerr << ans << '\n';
    }
    
    cout << ans << '\n';
    
    return 0;
}

T2:最小配对

只需找出前 \(O(n\log n)\) 小的组合即可
可以用小根堆来维护

代码实现

<details>
<summary>点击查看代码</summary>

include <bits/stdc++.h>

define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
int n;
cin >> n;

vector<int> a(n), b(n);
rep(i, n) cin >> a[i];
rep(i, n) cin >> b[i];

sort(a.begin(), a.end());
sort(b.begin(), b.end());

priority_queue<int, vector<int>, greater<int>> q;
rep(i, n) {
    for (int j = 0; (i+1)*(j+1) < n; ++j) {
        q.push(a[i]+b[j]);
    }
}

rep(i, n) {
    cout << q.top() << ' ';
    q.pop();
}

return 0;

}

</details>

T3:炼制合金

二维费用01背包

dp[i][j][k] 表示从前 \(i\) 块材料中挑选若干块材料满足总共有 \(j\) 克黄金和 \(k\) 克白银所需支付的最小费用

代码实现
#include <bits/extc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int dp[305][305];

inline void chmin(int& x, int y) { if (x > y) x = y; }

int main() {
    int n, a, b;
    cin >> n >> a >> b;
    
    const int INF = 1001001001;
    rep(i, a+1)rep(j, b+1) dp[i][j] = INF;
    dp[0][0] = 0;
    rep(i, n) {
        int x, y, w;
        cin >> x >> y >> w;
        for (int j = a; j >= 0; --j) {
            for (int k = b; k >= 0; --k) {
                int nj = min(a, j+x);
                int nk = min(b, k+y);
                chmin(dp[nj][nk], dp[j][k]+w);
            }
        }
    }
    
    cout << dp[a][b] << '\n';
    
    return 0;
}

T4:数三角形(二)

容斥原理

从网格中任选三点的方案数减去三点共线的方案数,即

\( \binom{(n+1)(m+1)}{3} - \binom{m+1}{3} \cdot (n+1) - \binom{n+1}{3} \cdot (m+1) \)

另外,还需减去斜着的三点共线的方案数
对于斜着的三点共线,自然有两个点在两端,另一个点在中间,由两端点可以确定唯一的矩形,在这个矩形中存在正对角线和反对角线,是完全对称的

于是原问题转化成了找有多少条正对角线,然后找矩形上的正对角线有多少个三元组
对于同类型的矩形,只需看这一个矩形的正对角线上有多少个三元组,再乘上这种矩形的数量

网格中 \(i \times j\) 的矩形有 \((n-i+1)(m-j+1)\)

那么一个 \(i \times j\) 的矩形中的正对角线上有多少个三元组呢?
需要借助斜率的思想来考虑,令 \(g = \gcd(i, j)\),可以得到 \(\frac{i}{g} = x\)\(\frac{j}{g} = y\)
容易发现 \(\frac{x}{y} = \frac{i}{j}\),斜率相同,说明 \((y, x)\) 和矩形正对角线两端点三点共线。又因为从矩形左上角出发走 \(g\) 步可以到达矩形右下角,所以中间有 \(g-1\) 个点。

代码实现
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

ll choose3(ll x) {
    return x*(x-1)*(x-2)/6;
}

int main() {
    int n, m;
    cin >> n >> m;
    n++; m++;
    
    ll s = n*m;
    ll ans = choose3(s);
    ans -= choose3(n)*m;
    ans -= choose3(m)*n;
    for (int i = 1; i < n; ++i) {
        for (int j = 1; j < m; ++j) {
            ll g = gcd(i, j)-1;
            ans -= g*(n-i)*(m-j)*2;
        }
    }
    
    cout << ans << '\n';
    
    return 0;
}