arr
是货币数组,其中的值都是正数。再给定一个正数 aim
。
每个值都认为是一张货币,返回组成 aim
的最少张数。
注意:因为是求张数,所以每张货币认为是相同或不同就不重要了。
假设 arr = [3,1,3,5,1,1,1,5,3,2]
则去重后的货币数组[3,1,5,2]
每种货币的张数数组[3,4,2,1]
尝试方式:讨论每个位置的货币用 i
张的情况,比如 0 位置的货币(3元)分别用 0 张、1 张、2 张和 3 张的情况。
//经典方法:货币不去重,讨论每张货币用和不用的情况
int minCoins(vector<int> &arr, int aim) {
return process(arr, 0, aim);
}
//暴力递归
//index位置开始往后货币随意选择,组成rest的最少张数
int process(vector<int> &arr, int index, int rest) {
if (rest < 0) {
return INT_MAX;
}
if (index == arr.size()) {
return rest == 0 ? 0 : INT_MAX;
} else {
//不使用index位置的货币
int p1 = process(arr, index + 1, rest);
//使用index位置的货币
int p2 = process(arr, index + 1, rest - arr[index]);
if (p2 != INT_MAX) {
p2++; //加上index位置这张货币
}
return min(p1, p2);
}
}
//暴力递归修改成动态规划
//O(arr长度 * aim)
int dp1(vector<int> &arr, int aim) {
if (aim == 0) return 0;
int n = arr.size();
int dp[n + 1][aim + 1];
memset(dp, 0, sizeof(dp));
dp[n][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[n][j] = INT_MAX;
}
for (int index = n - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int p1 = dp[index + 1][rest];
int p2 = rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : INT_MAX;
if (p2 != INT_MAX) {
p2++;
}
dp[index][rest] = min(p1, p2);
}
}
return dp[0][aim];
}
假设当前来到了 i i i 位置,而该面值是3元,一共2张, i i i 及其往后的货币自由选择,如果要组成目标货币27元,求最小张数。
分析该位置的求解需要依赖的值:
如果使用 0 张 i i i 位置的货币,则 i + 1 i + 1 i+1 及其后面的货币自由选择组成目标货币27元,如果需要 a a a 张;
而如果使用 1 张 i i i 位置的货币(3元), i + 1 i+1 i+1 及其后面的货币自由选择组成27 - 3 = 24元,假设需要 b b b 张,再加上这一张3元,总共就是 b + 1 b + 1 b+1 张;
如果使用 2 张 i i i 位置的货币(3元), i + 1 i + 1 i+1 及其后面的货币自由选择组成目标24 - 3 = 21元,假设需要 c 张,再加上这两张3元,总共就是 c + 2 c + 2 c+2 张;
待求的位置的结果是 min{a + 0, b + 1, c + 2}
,此过程涉及到了一个枚举行为。
代码实现如下:
class Info {
public:
vector<int> coins;
vector<int> zhangs;
Info(vector<int> &c, vector<int> &z) {
coins = c;
zhangs = z;
}
};
Info *getInfo(vector<int> &arr) { //得到去重货币数组以及货币张数
unordered_map<int, int> counts;
for (int value : arr) {
if (!counts.count(value)) {
counts[value] = 1;
} else {
counts[value] += 1;
}
}
int n = counts.size();
vector<int> coins(n);
vector<int> zhangs(n);
int index = 0;
for (auto entry : counts) {
coins[index] = entry.first;
zhangs[index++] = entry.second;
}
return new Info(coins, zhangs);
}
//优化:去重版动态规划
//时间复杂度:O(arr长度) + O(货币种数 * aim * 每种货币的平均张数)
int dp2(vector<int> &arr, int aim) {
if (aim == 0) return 0;
Info *info = getInfo(arr); //得到info的时间复杂度O(arr长度)
vector<int> coins = info->coins;
vector<int> zhangs= info->zhangs;
int n = coins.size();
int dp[n + 1][aim + 1];
memset(dp, 0, sizeof(dp));
dp[n][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[n][j] = INT_MAX;
}
//时间复杂度: O(货币种数 * aim * 每种货币的平均张数)
for (int index = n - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
for (int zhang = 1; zhang * coins[index] <= aim && zhang <= zhangs[index]; zhang++) { //枚举
if ((rest - zhang * coins[index] >= 0) && (dp[index + 1][rest - zhang * coins[index]] != INT_MAX)) {
dp[index][rest] = min(dp[index][rest], zhang + dp[index + 1][rest - zhang * coins[index]]);
}
}
}
}
return dp[0][aim];
}
int compensate(int pre, int cur, int coin) {
return (cur - pre) / coin;
}
能否省掉这个枚举行为?
看待求位置的前一个位置:同理上面的分析,X位置的结果是 min{b + 0, c + 1, d + 2}
根据斜率优化,想省掉这个枚举行为,但是麻烦了。因为待求位置是 min{a + 0, b + 1, c + 2}
,而x位置是 min{b + 0, c + 1, d + 2}
,也就是说 x 位置依赖的窗口是 [d, c, b]
,而待求位置依赖的窗口是 [c, b, a]
,x并不知道d
滑出窗口后,最小值如何变化。因为 x 并不知道 d + 2, b + 1, a + 0
三个值谁胜出了,x 只记录一个最小值,所以x不知道在d滑出窗口后,最小值如何变化。这就比如果求的依赖的累加和麻烦。
那么到底该怎么变呢?使用窗口内最大值和最小值的更新结构。
抽象化:准备一个单调双端队列,存放最小值,即从队首到队尾,值从小到大。如果当前窗口内放着 x 元 a 张(队列内放的张数),而目前 y 元 b 张要入队,那么应该谁和谁比较呢?【a + (y - x) / 面值
】 和 【b
】 进行比较,就是说在比较的时候,必须是基于钱数相等的情况下,面值小的货币张数就要先补齐到货币值等于钱数。
优化代码如下:
//时间复杂度: O(arr长度) + O(货币种数 * aim)
int dp3(vector<int> &arr, int aim) {
if (aim == 0) return 0;
Info *info = getInfo(arr);
vector<int> c = info->coins;
vector<int> z = info->zhangs;
int n = c.size();
//静态数组的局限性:占用的栈空间过大,导致无法申请声明数组
vector<vector<int> > dp(n + 1, vector<int>(aim + 1, 0));
dp[n][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[n][j] = INT_MAX;
}
// 虽然是嵌套了很多循环,但是时间复杂度为O(货币种数 * aim)
// 因为用了窗口内最小值的更新结构
for (int i = n - 1; i >= 0; i--) {
for (int mod = 0; mod < min(aim + 1, c[i]); mod++) {
// 当前面值 X
// mod mod + x mod + 2*x mod + 3 * x
deque<int> w;
w.push_back(mod);
dp[i][mod] = dp[i + 1][mod];
for (int r = mod + c[i]; r <= aim; r += c[i]) {
while (!w.empty() && (dp[i + 1][w.back()] == INT_MAX || dp[i + 1][w.back()] + compensate(w.back(), r, c[i]) >= dp[i + 1][r])) {
w.pop_back();
}
w.push_back(r);
int overdue = r - c[i] * (z[i] + 1);
if (w.front() == overdue) {
w.pop_front();
}
dp[i][r] = dp[i + 1][w.front()] + compensate(w.front(), r, c[i]);
}
}
}
return dp[0][aim];
}
优化的好处:如果不压缩数据(去重得到面值数组),那么填表的时间复杂度是 O ( n ∗ a i m ) O(n * aim) O(n∗aim);但是压缩后,假设原数组长度为 n n n,去重得到的面值数组长度为 m m m,那么只需要一个 m ∗ a i m m * aim m∗aim 的表即可(时间复杂度 O ( m ∗ a i m ) O(m * aim) O(m∗aim),且每个格子都是由窗口结构更新而来,时间复杂度为 O ( 1 ) O(1) O(1),当货币出现大量重复的时候,就非常省时间。
本题优化点:张数压缩;用窗口更新结构省掉枚举行为。
/*************************************************************************
> File Name: 004.组成目标货币的最少张数.cpp
> Author: Maureen
> Created Time: 三 11/16 15:12:39 2022
************************************************************************/
#include
#include
#include
#include
#include
using namespace std;
//暴力递归
//index位置开始往后货币随意选择,组成rest的最少张数
int process(vector<int> &arr, int index, int rest) {
if (rest < 0) {
return INT_MAX;
}
if (index == arr.size()) {
return rest == 0 ? 0 : INT_MAX;
} else {
//不使用index位置的货币
int p1 = process(arr, index + 1, rest);
//使用index位置的货币
int p2 = process(arr, index + 1, rest - arr[index]);
if (p2 != INT_MAX) {
p2++; //加上index位置这张货币
}
return min(p1, p2);
}
}
//方法1
//经典方法:货币不去重,讨论每张货币用和不用的情况
int minCoins(vector<int> &arr, int aim) {
return process(arr, 0, aim);
}
//方法2:
//暴力递归修改成动态规划
//O(arr长度 * aim)
int dp1(vector<int> &arr, int aim) {
if (aim == 0) return 0;
int n = arr.size();
int dp[n + 1][aim + 1];
memset(dp, 0, sizeof(dp));
dp[n][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[n][j] = INT_MAX;
}
for (int index = n - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int p1 = dp[index + 1][rest];
int p2 = rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : INT_MAX;
if (p2 != INT_MAX) {
p2++;
}
dp[index][rest] = min(p1, p2);
}
}
return dp[0][aim];
}
class Info {
public:
vector<int> coins;
vector<int> zhangs;
Info(vector<int> &c, vector<int> &z) {
coins = c;
zhangs = z;
}
};
Info *getInfo(vector<int> &arr) { //得到去重货币数组以及货币张数
unordered_map<int, int> counts;
for (int value : arr) {
if (!counts.count(value)) {
counts[value] = 1;
} else {
counts[value] += 1;
}
}
int n = counts.size();
vector<int> coins(n);
vector<int> zhangs(n);
int index = 0;
for (auto entry : counts) {
coins[index] = entry.first;
zhangs[index++] = entry.second;
}
return new Info(coins, zhangs);
}
//方法3
//优化:去重版动态规划
//时间复杂度:O(arr长度) + O(货币种数 * aim * 每种货币的平均张数)
int dp2(vector<int> &arr, int aim) {
if (aim == 0) return 0;
Info *info = getInfo(arr); //得到info的时间复杂度O(arr长度)
vector<int> coins = info->coins;
vector<int> zhangs= info->zhangs;
int n = coins.size();
int dp[n + 1][aim + 1];
memset(dp, 0, sizeof(dp));
dp[n][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[n][j] = INT_MAX;
}
//时间复杂度: O(货币种数 * aim * 每种货币的平均张数)
for (int index = n - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
for (int zhang = 1; zhang * coins[index] <= aim && zhang <= zhangs[index]; zhang++) { //枚举
if ((rest - zhang * coins[index] >= 0) && (dp[index + 1][rest - zhang * coins[index]] != INT_MAX)) {
dp[index][rest] = min(dp[index][rest], zhang + dp[index + 1][rest - zhang * coins[index]]);
}
}
}
}
return dp[0][aim];
}
int compensate(int pre, int cur, int coin) {
return (cur - pre) / coin;
}
//方法4
//优化需要用到窗口内最小值的更新结构
//时间复杂度: O(arr长度) + O(货币种数 * aim)
int dp3(vector<int> &arr, int aim) {
if (aim == 0) return 0;
Info *info = getInfo(arr);
vector<int> c = info->coins;
vector<int> z = info->zhangs;
int n = c.size();
//静态数组的局限性:占用的栈空间过大,导致无法申请声明数组
vector<vector<int> > dp(n + 1, vector<int>(aim + 1, 0));
dp[n][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[n][j] = INT_MAX;
}
// 虽然是嵌套了很多循环,但是时间复杂度为O(货币种数 * aim)
// 因为用了窗口内最小值的更新结构
for (int i = n - 1; i >= 0; i--) {
for (int mod = 0; mod < min(aim + 1, c[i]); mod++) {
// 当前面值 X
// mod mod + x mod + 2*x mod + 3 * x
deque<int> w;
w.push_back(mod);
dp[i][mod] = dp[i + 1][mod];
for (int r = mod + c[i]; r <= aim; r += c[i]) {
while (!w.empty() && (dp[i + 1][w.back()] == INT_MAX || dp[i + 1][w.back()] + compensate(w.back(), r, c[i]) >= dp[i + 1][r])) {
w.pop_back();
}
w.push_back(r);
int overdue = r - c[i] * (z[i] + 1);
if (w.front() == overdue) {
w.pop_front();
}
dp[i][r] = dp[i + 1][w.front()] + compensate(w.front(), r, c[i]);
}
}
}
return dp[0][aim];
}
//测试代码
vector<int> randomArray(int n, int maxValue) {
vector<int> arr(n);
for (int i = 0; i < n; i++) {
arr[i] = (rand() % maxValue) + 1;
}
return arr;
}
void printArray(vector<int> &arr) {
cout << "arr : ";
for (int i = 0; i < arr.size(); i++) {
cout << arr[i] << " ";
}
cout << endl;
}
int main() {
int maxLen = 20;
int maxValue = 30;
int testTime = 300000 + 1;
cout << "Function Test begin:" << endl;
for (int i = 0; i < testTime; i++) {
int n = rand() % maxLen;
vector<int> arr = randomArray(n, maxValue);
int aim = rand() % maxValue;
int ans = minCoins(arr, aim);
int ans1 = dp1(arr, aim);
int ans2 = dp2(arr, aim);
int ans3 = dp3(arr, aim);
if (ans != ans1 || ans2 != ans3 || ans != ans2) {
cout << "Oops!" << endl;
printArray(arr);
cout << "aim = " << aim << endl;
cout << "ans = " << ans << endl;
cout << "ans1 = " << ans1 << endl;
cout << "ans2 = " << ans2 << endl;
cout << "ans3 = " << ans3 << endl;
break;
}
if (i % 10000 == 0) cout << i << " cases passed!" << endl;
}
cout << "Function Test ends!" << endl;
cout << "===============" << endl;
int aim = 0;
vector<int> arr;
long long start;
long long end;
int ans2;
int ans3;
cout << "Performance Test begin:" << endl;
maxLen = 30000;
maxValue = 20;
aim = 60000;
arr = randomArray(maxLen, maxValue);
start = clock();
ans2 = dp2(arr, aim);
end = clock();
cout << "ans2 = " << ans2 << ", dp2 cost " << (end - start) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
start = clock();
ans3 = dp3(arr, aim);
end = clock();
cout << "ans3 = " << ans3 << ", dp3 cost " << (end - start) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
cout << "Performance Test ends!" << endl;
cout << "===============" << endl;
//以下测试的数据量只有dp3能通过,其它方法不能通过
cout << "货币大量重复出现的情况下" << endl;
cout << "大数据量测试滑动窗口方案开始:"<< endl;
maxLen = 20000000;
aim = 10000;
maxValue = 10000;
arr = randomArray(maxLen, maxValue);
start = clock();
ans3 = dp3(arr, aim);
end = clock();
cout << "dp3滑动窗口方案运行时间:" << (end - start) * 1000 / CLOCKS_PER_SEC << " ms" << endl;
cout << "大数据量测试滑动窗口方案结束" << endl;
cout << "===============" << endl;
cout << "当货币很少出现重复,dp2比dp3有常数时间优势" << endl;
cout << "当货币大量出现重复,dp3时间复杂度明显优于dp2" << endl;
cout << "dp3的优化用到了窗口最小值的更新结构" << endl;
return 0;
}
public class Code04_MinCoinsOnePaper {
public static int minCoins(int[] arr, int aim) {
return process(arr, 0, aim);
}
//经典方法:货币不去重,讨论每张货币用和不用的情况
//暴力递归
public static int process(int[] arr, int index, int rest) {
if (rest < 0) {
return Integer.MAX_VALUE;
}
if (index == arr.length) {
return rest == 0 ? 0 : Integer.MAX_VALUE;
} else {
int p1 = process(arr, index + 1, rest);
int p2 = process(arr, index + 1, rest - arr[index]);
if (p2 != Integer.MAX_VALUE) {
p2++;
}
return Math.min(p1, p2);
}
}
//暴力递归改成的动态规划
// dp1时间复杂度为:O(arr长度 * aim)
public static int dp1(int[] arr, int aim) {
if (aim == 0) {
return 0;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[N][j] = Integer.MAX_VALUE;
}
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int p1 = dp[index + 1][rest];
int p2 = rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : Integer.MAX_VALUE;
if (p2 != Integer.MAX_VALUE) {
p2++;
}
dp[index][rest] = Math.min(p1, p2);
}
}
return dp[0][aim];
}
public static class Info {
public int[] coins; //所有货币去重后的数组
public int[] zhangs; //每种货币的张数
public Info(int[] c, int[] z) {
coins = c;
zhangs = z;
}
}
public static Info getInfo(int[] arr) {
HashMap<Integer, Integer> counts = new HashMap<>();
for (int value : arr) {
if (!counts.containsKey(value)) {
counts.put(value, 1);
} else {
counts.put(value, counts.get(value) + 1);
}
}
int N = counts.size();
int[] coins = new int[N];
int[] zhangs = new int[N];
int index = 0;
for (Entry<Integer, Integer> entry : counts.entrySet()) {
coins[index] = entry.getKey();
zhangs[index++] = entry.getValue();
}
return new Info(coins, zhangs);
}
// dp2时间复杂度为:O(arr长度) + O(货币种数 * aim * 每种货币的平均张数)
public static int dp2(int[] arr, int aim) {
if (aim == 0) {
return 0;
}
// 得到info时间复杂度O(arr长度)
Info info = getInfo(arr);
int[] coins = info.coins;
int[] zhangs = info.zhangs;
int N = coins.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[N][j] = Integer.MAX_VALUE;
}
// 这三层for循环,时间复杂度为O(货币种数 * aim * 每种货币的平均张数)
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
for (int zhang = 1; zhang * coins[index] <= aim && zhang <= zhangs[index]; zhang++) { //枚举行为
if (rest - zhang * coins[index] >= 0
&& dp[index + 1][rest - zhang * coins[index]] != Integer.MAX_VALUE) {
dp[index][rest] = Math.min(dp[index][rest], zhang + dp[index + 1][rest - zhang * coins[index]]);
}
}
}
}
return dp[0][aim];
}
// dp3时间复杂度为:O(arr长度) + O(货币种数 * aim)
// 优化需要用到窗口内最小值的更新结构
public static int dp3(int[] arr, int aim) {
if (aim == 0) {
return 0;
}
// 得到info时间复杂度O(arr长度)
Info info = getInfo(arr);
int[] c = info.coins;
int[] z = info.zhangs;
int N = c.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 0;
for (int j = 1; j <= aim; j++) {
dp[N][j] = Integer.MAX_VALUE;
}
// 虽然是嵌套了很多循环,但是时间复杂度为O(货币种数 * aim)
// 因为用了窗口内最小值的更新结构
for (int i = N - 1; i >= 0; i--) {
//例如目标货币是30元,而当前货币面值是7元,则先填0,7, 14, 21, 28;然后填1, 8, 15, 22, 29... 分组填
for (int mod = 0; mod < Math.min(aim + 1, c[i]); mod++) {
// 当前面值 X
// mod mod + x mod + 2*x mod + 3 * x
LinkedList<Integer> w = new LinkedList<>();
w.add(mod);
dp[i][mod] = dp[i + 1][mod];
for (int r = mod + c[i]; r <= aim; r += c[i]) {
while (!w.isEmpty() && (dp[i + 1][w.peekLast()] == Integer.MAX_VALUE
|| dp[i + 1][w.peekLast()] + compensate(w.peekLast(), r, c[i]) >= dp[i + 1][r])) { //入队列的时候要跟补偿后的张数进行比较
w.pollLast();
}
w.addLast(r);
int overdue = r - c[i] * (z[i] + 1);
if (w.peekFirst() == overdue) {
w.pollFirst();
}
dp[i][r] = dp[i + 1][w.peekFirst()] + compensate(w.peekFirst(), r, c[i]); //得到最后结果的时候也要进行补偿
}
}
}
return dp[0][aim];
}
public static int compensate(int pre, int cur, int coin) {
return (cur - pre) / coin;
}
// 为了测试
public static int[] randomArray(int N, int maxValue) {
int[] arr = new int[N];
for (int i = 0; i < N; i++) {
arr[i] = (int) (Math.random() * maxValue) + 1;
}
return arr;
}
// 为了测试
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 为了测试
public static void main(String[] args) {
int maxLen = 20;
int maxValue = 30;
int testTime = 300000;
System.out.println("功能测试开始");
for (int i = 0; i < testTime; i++) {
int N = (int) (Math.random() * maxLen);
int[] arr = randomArray(N, maxValue);
int aim = (int) (Math.random() * maxValue);
int ans1 = minCoins(arr, aim);
int ans2 = dp1(arr, aim);
int ans3 = dp2(arr, aim);
int ans4 = dp3(arr, aim);
if (ans1 != ans2 || ans3 != ans4 || ans1 != ans3) {
System.out.println("Oops!");
printArray(arr);
System.out.println(aim);
System.out.println(ans1);
System.out.println(ans2);
System.out.println(ans3);
System.out.println(ans4);
break;
}
}
System.out.println("功能测试结束");
System.out.println("==========");
int aim = 0;
int[] arr = null;
long start;
long end;
int ans2;
int ans3;
System.out.println("性能测试开始");
maxLen = 30000;
maxValue = 20;
aim = 60000;
arr = randomArray(maxLen, maxValue);
start = System.currentTimeMillis();
ans2 = dp2(arr, aim);
end = System.currentTimeMillis();
System.out.println("dp2答案 : " + ans2 + ", dp2运行时间 : " + (end - start) + " ms");
start = System.currentTimeMillis();
ans3 = dp3(arr, aim);
end = System.currentTimeMillis();
System.out.println("dp3答案 : " + ans3 + ", dp3运行时间 : " + (end - start) + " ms");
System.out.println("性能测试结束");
System.out.println("===========");
System.out.println("货币大量重复出现情况下,");
System.out.println("大数据量测试dp3开始");
maxLen = 20000000;
aim = 10000;
maxValue = 10000;
arr = randomArray(maxLen, maxValue);
start = System.currentTimeMillis();
ans3 = dp3(arr, aim);
end = System.currentTimeMillis();
System.out.println("dp3运行时间 : " + (end - start) + " ms");
System.out.println("大数据量测试dp3结束");
System.out.println("===========");
System.out.println("当货币很少出现重复,dp2比dp3有常数时间优势");
System.out.println("当货币大量出现重复,dp3时间复杂度明显优于dp2");
System.out.println("dp3的优化用到了窗口内最小值的更新结构");
}
}