ABC182 F Valid payments
解法一
首先不妨来思考一下怎样的一个付钱方案是最优的,假设需要支付 \(Y\) 元,第 \(a_i\) 种钱币支付了 \(s_i\) 张,那么必须有:\(s_i < \frac{a_{i + 1}}{a_i}\)。
否则一定可以通过调整减少钱币数量,那么若满足这个条件,付钱方案就是最优的了。
因为你付的钱和售货员找的钱当中一定存在一个 \(s_i = 0\),那么我们可以考虑将售货员找的钱取反就能恰好通过一个 \(s\) 序列表示一种可行的支付方案了。
于是问题转化为这样一个问题,求 \(s\) 序列的数量,满足:
同时通过这一条件,还可以推出 \(\forall i, \sum\limits_{j < i} a_j \times s_j < a_i\)。
因此,\(\forall i\) 应该要满足:\(|\sum\limits_{j \ge i} a_j \times s_j - X| < a_i\),注意到 \(a_i \mid \sum\limits_{j \ge i} a_j \times s_j\),因此这里的 \(\sum\limits_{j \ge i} a_j \times s_j\) 至多只有两种取值。
那么我们可以先计算出 \(\forall i, \sum\limits_{j \ge i} a_j \times s_j\) 的取值,然后就可以基于取值很少设计一个 \(dp\) 了。
不难发现可以令 \(f_{i, j}\) 表示 \(\sum\limits_{k \ge i} a_k \times s_k = j\) 时的方案数,转移时只需判断前后两个后缀之差这个位置是否能组成即可。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
const int N = 50 + 5;
int n, X, ans, a[N], cnt[N], b[N][3], dp[N][3];
signed main () {
cin >> n >> X;
rep(i, 1, n) {
cin >> a[i];
int L = ceil(1.0 * (X - a[i] + 1) / a[i]), R = (X + a[i] - 1) / a[i];
rep(j, L, R) b[i][++cnt[i]] = j * a[i];
}
rep(i, 1, cnt[n]) dp[n][i] = 1;
dep(i, 1, n - 1)
rep(j, 1, cnt[i])
rep(k, 1, cnt[i + 1])
if(abs(b[i + 1][k] - b[i][j]) / a[i] < a[i + 1] / a[i])
dp[i][j] += dp[i + 1][k];
rep(i, 1, cnt[1]) ans += dp[1][i];
cout << ans;
return 0;
}
解法二
同样需要解法一当中付钱方案最优的思考,接下来你会发现这个东西本质上是一个类似进制的东西。
假设 \(Y, Z\) 分别为你付的钱,收获员找的钱,那么应该满足 \(X + Z = Y\)
将这个东西类似于十进制加法一样在这个类进制下考虑,你会发现 \(Z, Y\) 中在某一位必须至少有一个为 \(0\)。
若 \(X\) 这位也为 \(0\) 那么 \(Z\) 这一位只能填 \(0\),否则 \(Z\) 只能填一个使得这一位恰好进位的数。
那么此时本质上之前位填的数对当前位的影响只有进位或不进位两种,所以可以令 \(f_{i, 0 / 1}\) 表示考虑到第 \(i\) 位之前有没有进位的方案数即可。
关于(类)进制的一条重要性质:\(\forall i, \sum\limits_{j < i} a_j \times s_j < a_i\)
另外这种涉及某种进制在一些位取值不同的情况下的运算时,一定要有类似于十进制数加法运算的思考。