题目描述
计算机刚刚兴起时,移动存储通常采用软盘。软盘的优点是便宜,但是容量很小。
现在有一张精美图片,大小为 d 兆,同时阿甘手中有 n 种不同容量的软盘,假设第 i 种软盘的容量为 2^i 兆,有 N[i] 个。
请你帮助阿甘分配软盘,实现图片最多拷贝的同时软盘减少浪费,并告诉阿甘可以拷贝多少份图片,以及剩余未使用的软盘数量。
注意:图片可以在压缩后,进行分割,得到多个子压缩包。一个软盘只能用于存储同一个图片文件的子压缩包。
输入描述
第一行输入图片的大小 d 兆。
第二行输入 n 个正整数,表示 n 种不同容量的软盘的数量,其中第 i 种软盘的容量为 2^i。以逗号分隔。n 不大于 30。
输出描述
第一行输出最多可拷贝几份图片。
第二行输出剩余软盘数量,格式见用例。
用例
输入 |
18 [0, 2, 2, 9, 8] |
输出 | 9 [0, 0, 0, 2, 0] |
说明 |
有 1 兆软盘 0 个。 有 2 兆软盘 2 个。 有 4 兆软盘 2 个。 有 8 兆软盘 9 个。 有 16 兆软盘 8 个。 其中 16 + 2 = 18,因此消耗2个2兆软盘,和2个16兆软盘可以拷贝2份图片。 16 + 4 = 20,因此消耗2个4兆软盘,和2个16兆软盘可以拷贝2份图片。 16 + 8 = 24,因此消耗4个8兆软盘,和4个16兆软盘可以拷贝4份图片。 8 + 8 + 8 = 24,因此可以消耗3个8兆软盘,拷贝1分图片。 一共可以拷贝出9份图片。剩余2个8兆软盘未使用。 |
题目解析
想要实现图片的最多拷贝,那么就要尽可能构造出容量接近于(≥) d 的软盘组合。
对于用例:
30
[10, 5, 0, 1, 3, 2]
图片大小 d = 30 兆,其可以分解为:16 + 8 + 4 + 2,因此我们可以选择下面软盘组合来实现无浪费地存储
1 个 2^4 兆的软盘,1 个 2^3 兆的软盘,1 个 2^2 的软盘,1 个 2^1 的软盘。
但是由于没有 2^2 兆的软盘,因此我们可以将对于 2^2 兆软盘的需求降级,变为 2 个 2^1 兆的软盘,也就是说最终选择是:
1 个 2^4 兆的软盘,1 个 2^3 兆的软盘,3 个 2^1 兆的软盘。
即 16 + 8 + 3 * 2 = 30
另外,如果单个软盘容量就能满足 d,比如 2^5 兆(32兆)的软盘也可以存储 d = 30 兆的图片,虽然此时浪费了一些,但是一个软盘只能给一个图片,因此为了避免更大的浪费,32 兆软盘就可以单独组合,而不需要组合其他软盘使用。
那么如何能准确的构造出容量为 30 兆的软盘组合呢?
首先,我们将 d 值转为二进制,然后再反序,让 d 和软盘列表 N 的阶数方向(从左到右,依次是 2^0, 2^1, ..., 2^i)保持一致,
比如 d=30,可以转为 [0, 1, 1, 1, 1] 的反序二进制值的个数数组,该数组的含义是
- 2^0有0个
- 2^1有1个
- 2^2有1个
- 2^3有1个
- 2^4有1个
而输入的第二行 N 列表,也可以看成反序二进制数的个数数组: [10, 5, 0, 1, 3, 2]
该数组的含义是:
- 2^0有10个
- 2^1有5个
- 2^2有0个
- 2^3有1个
- 2^4有3个
- 2^5有2个
如果想从 N 列表中选出和为 d 的软盘组合,那么不就是 N 和 d 的二进制值个数求差吗?
我们只需要比较D.length范围的内,而超出D.length范围的N[i]其实就是单个软盘就足以满足一个图片存储。
如上图所示,如果每次求差后,N[0] >= 0,则表示可以构造出一个和为30的软盘组合。
但是N[0] < 0 只能表示无法构造出一个和准确为30的的软盘组合,但是却还是有可能构造出一个和大于30的信道组合
程序输入
47
[10, 5, 0, 1, 3, 2]
对应的N和D如下
直到N[i]完全抵消了负值结束,然后count++。也可能N[i]到最后也没有抵消完负值,此时说明无法构造出一个和大于30的软盘组合,整个程序结束。
2023.10.10
上面举得例子
47
[10, 5, 0, 1, 3, 2]
进行二进制减法时存在问题
如下图所示,在进行第二次减法时
之前的策略时,总是向低位借,因此最后借债都会压到被减数的阶0上,之后再将阶0上的借债,转移不停转移到高位,因此会得到结果如下
但是这种策略不是最优的,因为我们可以看下图
其中 被减数的标黄部分,经过计算,是可以确定小于 减数的标黄部分的,因此即使我们向低位借,最终还是不够,最后反过来还是要向高位借。
而二进制数,一个高位是可以直接满足前面的所有低位的,什么意思呢?看下图
我们此时直接从被减数红框高位中借1,就可以满足 减数红框中所有位之和。
此时被减数的 绿色低位部分 得到了保留,避免了浪费。
Python算法源码
# 输入获取
D = int(input())
N = eval(input())
def calc_sub(bin1):
ans = 0
for i in range(len(bin1)):
ans += bin1[i] * (1 << i)
return ans
def binary_sub(minuend, subtrahend):
"""
二进制减法
:param minuend: 被减数
:param subtrahend: 减数
:return: 被减数是否为正数
"""
# 进行减法运算逻辑, 从高位开始
i = len(minuend) - 1
while i >= 0:
if minuend[i] >= subtrahend[i]:
# 如果对应位的软盘数足够,则直接相减
minuend[i] -= subtrahend[i]
else:
# 如果对应位的软盘数不足,此时有两种策略,一是向低位借,一是向高位借
# 具体向哪里借,需要看 minuend 的 [0,i] 低位部分是否能够承载 subtrahend[0, i] 低位部分
if calc_sub(minuend[0:i + 1]) < calc_sub(subtrahend[0:i + 1]):
# 如果minuend 的 [0,i]不能承载,则向高位借,即从j=i+1位开始借
j = i + 1
while j < len(minuend):
if minuend[j] > 0:
# 如果高位 j 有软盘可借,则借
minuend[j] -= 1
return True
else:
# 否则继续向更高位探索
minuend[j] += subtrahend[j]
j += 1
# 如果所有高位都没有富余软盘数,则说明减法结果为负数
return False
else:
# 如果minuend 的 [0,i]可以承载,则向低位借(可以避免浪费)
# 此时minuend[i]为负数,表示欠债
minuend[i] -= subtrahend[i]
# 将当前阶位的欠债,转移到前面的低阶位上,注意转移时,欠债x2
minuend[i - 1] += minuend[i] << 1
# 转移后,当前阶位的欠债变为0
minuend[i] = 0
i -= 1
return True
if __name__ == '__main__':
# 将D值转化为二进制形式,并且为了和N[]的阶位进行对应,这里将D的二进制进行了反转
subtrahend = list(map(int, str(bin(D))[2:]))
subtrahend.reverse()
# count记录N能承载几个D
count = 0
# N中高阶软盘的单个软盘就能满足D,因此这些高阶软盘有几个,即能承载几个D
for i in range(len(subtrahend), len(N)):
# R ~ subtrahend.length 阶的单个软盘就能承载一个D,因此这些软盘有几个,就能承载几个D
count += N[i]
N[i] = 0
# 0 ~ subtrahend.length - 1 阶的单个软盘无法承载一个D,因此这些阶需要组合起来才能承载一个D
minuend = N[0:len(subtrahend)]
while len(minuend) < len(subtrahend):
minuend.append(0)
# 进行二进制减法
while binary_sub(minuend, subtrahend):
count += 1
for i in range(min(len(subtrahend), len(N))):
N[i] = minuend[i]
print(count)
print(f"[{", ".join(map(str, N))}]")
Java算法源码
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int D = Integer.parseInt(sc.nextLine());
int[] N = parse(sc.nextLine());
// 将D值转化为二进制形式,并且为了和N的阶位进行对应,这里将D的二进制进行了反转
int[] subtrahend =
Arrays.stream(new StringBuilder(Integer.toBinaryString(D)).reverse().toString().split(""))
.mapToInt(Integer::parseInt)
.toArray();
// count记录N能承载几个D
int count = 0;
// N中高阶软盘的单个软盘就能满足D,因此这些高阶软盘有几个,即能承载几个D
for (int i = N.length - 1; i >= subtrahend.length; i--) {
// R ~ subtrahend.length 阶的单个软盘就能承载一个D,因此这些软盘有几个,就能承载几个D
count += N[i];
N[i] = 0;
}
// 0 ~ subtrahend.length - 1 阶的单个软盘无法承载一个D,因此这些阶需要组合起来才能承载一个D
int[] minuend = Arrays.copyOfRange(N, 0, subtrahend.length);
// 进行二进制减法
while (binary_sub(minuend, subtrahend)) {
count++;
}
for (int i = 0; i < Math.min(subtrahend.length, N.length); i++) {
N[i] = minuend[i];
}
System.out.println(count);
System.out.println(Arrays.toString(N));
}
public static int[] parse(String s) {
return Arrays.stream(s.substring(1, s.length() - 1).split(", ")).mapToInt(Integer::parseInt).toArray();
}
/**
* 二进制减法
*
* @param minuend 被减数
* @param subtrahend 减数
* @return 被减数是否为正数
*/
public static boolean binary_sub(int[] minuend, int[] subtrahend) {
// 进行减法运算逻辑, 从高位开始
for (int i = minuend.length - 1; i >= 0; i--) {
if (minuend[i] >= subtrahend[i]) {
// 如果对应位的软盘数足够,则直接相减
minuend[i] -= subtrahend[i];
} else {
// 如果对应位的软盘数不足,此时有两种策略,一是向低位借,一是向高位借
// 具体向哪里借,需要看 minuend 的 [0,i] 低位部分是否能够承载 subtrahend[0, i] 低位部分
if (calc_bin(Arrays.copyOfRange(minuend, 0, i + 1))
< calc_bin(Arrays.copyOfRange(subtrahend, 0, i + 1))) {
// 如果minuend 的 [0,i]不能承载,则向高位借,即从j=i+1位开始借
int j = i + 1;
while (j < minuend.length) {
if (minuend[j] > 0) {
// 如果高位 j 有软盘可借,则借
minuend[j] -= 1;
return true;
} else {
// 否则继续向更高位探索
minuend[j] += subtrahend[j];
j += 1;
}
}
// 如果所有高位都没有富余软盘数,则说明减法结果为负数
return false;
} else {
// 如果minuend 的 [0,i]可以承载,则向低位借(向低位借,可以避免浪费)
// 此时minuend[i]为负数,表示欠债
minuend[i] -= subtrahend[i];
// 将当前阶位的欠债,转移到前面的低阶位上,注意转移时,欠债x2
minuend[i - 1] += minuend[i] << 1;
// 转移后,当前阶位的欠债变为0
minuend[i] = 0;
}
}
}
return true;
}
public static int calc_bin(int[] bin) {
int ans = 0;
for (int i = 0; i < bin.length; i++) {
ans += bin[i] * (1 << i);
}
return ans;
}
}
JS算法源码
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
void (async function () {
const D = parseInt(await readline());
const N = JSON.parse(await readline());
// 将D值转化为二进制形式,并且为了和N[]的阶位进行对应,这里将D的二进制进行了反转
const subtrahend = Number(D).toString(2).split("").map(Number).reverse();
// count记录N能承载几个D
let count = 0;
// N中高阶软盘的单个软盘就能满足D,因此这些高阶软盘有几个,即能承载几个D
for (let i = N.length - 1; i >= subtrahend.length; i--) {
// R ~ subtrahend.length 阶的单个软盘就能承载一个D,因此这些软盘有几个,就能承载几个D
count += N[i];
N[i] = 0;
}
// 0 ~ subtrahend.length - 1 阶的单个软盘无法承载一个D,因此这些阶需要组合起来才能承载一个D
const minuend = N.slice(0, subtrahend.length);
while (minuend.length < subtrahend.length) {
minuend.push(0);
}
// 进行二进制减法
while (binary_sub(minuend, subtrahend)) {
count++;
}
for (let i = 0; i < Math.min(subtrahend.length, N.length); i++) {
N[i] = minuend[i];
}
console.log(count);
console.log(`[${N.join(", ")}]`);
})();
/**
* 进行二进制减法
* @param {*} minuend 被减数
* @param {*} subtrahend 减数
* @returns 被减数是否为正数
*/
function binary_sub(minuend, subtrahend) {
// 进行减法运算逻辑, 从高位开始
for (let i = minuend.length - 1; i >= 0; i--) {
if (minuend[i] >= subtrahend[i]) {
// 如果对应位的软盘数足够,则直接相减
minuend[i] -= subtrahend[i];
} else {
// 如果对应位的软盘数不足,此时有两种策略,一是向低位借,一是向高位借
// 具体向哪里借,需要看 minuend 的 [0,i] 低位部分是否能够承载 subtrahend[0, i] 低位部分
if (
calc_sub(minuend.slice(0, i + 1)) < calc_sub(subtrahend.slice(0, i + 1))
) {
// 如果minuend 的 [0,i]不能承载,则向高位借,即从j=i+1位开始借
let j = i + 1;
while (j < minuend.length) {
if (minuend[j] > 0) {
// 如果高位 j 有软盘可借,则借
minuend[j] -= 1;
return true;
} else {
// 否则继续向更高位探索
minuend[j] += subtrahend[j];
j += 1;
}
}
// 如果所有高位都没有富余软盘数,则说明减法结果为负数
return false;
} else {
// 如果minuend 的 [0,i]可以承载,则向低位借(向低位借,可以避免浪费)
// 此时minuend[i]为负数,表示欠债
minuend[i] -= subtrahend[i];
// 将当前阶位的欠债,转移到前面的低阶位上,注意转移时,欠债x2
minuend[i - 1] += minuend[i] << 1;
// 转移后,当前阶位的欠债变为0
minuend[i] = 0;
}
}
}
return true;
}
function calc_sub(bin) {
let ans = 0;
for (let i = 0; i < bin.length; i++) {
ans += bin[i] * (1 << i);
}
return ans;
}
C算法源码
#include <stdio.h>
#include <math.h>
#define MAX_SIZE 50
int calc_bin(const int bin[], int bin_size) {
int ans = 0;
for (int i = 0; i < bin_size; i++) {
ans += bin[i] * (1 << i);
}
return ans;
}
/*!
*
* @param minuend 被减数
* @param subtrahend 减数
* @param size 被减数和件数的二进制位数
* @return 减法运算后,被减数是否为正数
*/
int binary_sub(int minuend[], int subtrahend[], int size) {
// 进行减法运算逻辑, 从高位开始
for (int i = size - 1; i >= 0; i--) {
if (minuend[i] >= subtrahend[i]) {
// 如果对应位的软盘数足够,则直接相减
minuend[i] -= subtrahend[i];
} else {
// 如果对应位的软盘数不足,此时有两种策略,一是向低位借,一是向高位借
// 具体向哪里借,需要看 minuend 的 [0,i] 低位部分是否能够承载 subtrahend[0, i] 低位部分
if (calc_bin(minuend, i + 1) < calc_bin(subtrahend, i + 1)) {
// 如果minuend 的 [0,i]不能承载,则向高位借,即从j=i+1位开始借
int j = i + 1;
while (j < size) {
if (minuend[j] > 0) {
// 如果高位 j 有软盘可借,则借
minuend[j] -= 1;
return 1;
} else {
// 否则继续向更高位探索
minuend[j] += subtrahend[j];
j += 1;
}
}
// 如果所有高位都没有富余软盘数,则说明减法结果为负数
return 0;
} else {
// 如果minuend 的 [0,i]可以承载,则向低位借(向低位借,可以避免浪费)
// 此时minuend[i]为负数,表示欠债
minuend[i] -= subtrahend[i];
// 将当前阶位的欠债,转移到前面的低阶位上,注意转移时,欠债x2
minuend[i - 1] += minuend[i] << 1;
// 转移后,当前阶位的欠债变为0
minuend[i] = 0;
}
}
}
return 1;
}
int main() {
int D;
scanf("%d", &D);
int N[MAX_SIZE] = {0};
int N_size = 0;
getchar();
getchar();
while (scanf("%d", &N[N_size++])) {
if (getchar() != ',') break;
getchar();
}
int subtrahend[MAX_SIZE];
int subtrahend_size = 0;
// 将D值转化为二进制形式,并且为了和N[]的阶位进行对应,这里将D的二进制进行了反转
while (D > 0) {
subtrahend[subtrahend_size++] = D % 2;
D /= 2;
}
// count记录N能承载几个D
int count = 0;
// N中高阶软盘的单个软盘就能满足D,因此这些高阶软盘有几个,即能承载几个D
for (int i = N_size - 1; i >= subtrahend_size; i--) {
// R ~ subtrahend.length 阶的单个软盘就能承载一个D,因此这些软盘有几个,就能承载几个D
count += N[i];
N[i] = 0;
}
// 0 ~ subtrahend.length - 1 阶的单个软盘无法承载一个D,因此这些阶需要组合起来才能承载一个D
int minuend[subtrahend_size];
for (int i = 0; i < subtrahend_size; i++) {
minuend[i] = N[i];
}
// 进行二进制减法
while (binary_sub(minuend, subtrahend, subtrahend_size)) {
count++;
}
for (int i = 0; i < fmin(subtrahend_size, N_size); i++) {
N[i] = minuend[i];
}
printf("%d\n", count);
printf("[");
for (int i = 0; i < N_size; i++) {
printf("%d", N[i]);
if (i < N_size - 1) {
printf(", ");
}
}
printf("]");
return 0;
}
C++算法源码
#include <bits/stdc++.h>
using namespace std;
// 二进制转十进制
int bin2dec(vector<int> bin) {
int ans = 0;
for (int i = 0; i < bin.size(); i++) {
ans += bin[i] * (1 << i);
}
return ans;
}
// 十进制转二进制
vector<int> dec2bin(int dec) {
vector<int> bin;
while (dec > 0) {
bin.emplace_back(dec % 2);
dec >>= 1;
}
return bin;
}
/*!
* 二进制减法
* @param minuend 被减数
* @param subtrahend 减数
* @return 被减数是否为正数
*/
bool binarySub(vector<int> &minuend, vector<int> &subtrahend) {
// 进行减法运算逻辑, 从高位开始
for (int i = minuend.size() - 1; i >= 0; i--) {
if (minuend[i] >= subtrahend[i]) {
// 如果对应位的软盘数足够,则直接相减
minuend[i] -= subtrahend[i];
} else {
// 如果对应位的软盘数不足,此时有两种策略,一是向低位借,一是向高位借
// 具体向哪里借,需要看 minuend 的 [0,i] 低位部分是否能够承载 subtrahend[0, i] 低位部分
if (bin2dec(vector<int>(minuend.begin(), minuend.begin() + i + 1)) <
bin2dec(vector<int>(subtrahend.begin(), subtrahend.begin() + i + 1))) {
// 如果minuend 的 [0,i]不能承载,则向高位借,即从j=i+1位开始借
int j = i + 1;
while (j < minuend.size()) {
if (minuend[j] > 0) {
// 如果高位 j 有软盘可借,则借
minuend[j] -= 1;
return true;
} else {
// 否则继续向更高位探索
minuend[j] += subtrahend[j];
j += 1;
}
}
// 如果所有高位都没有富余软盘数,则说明减法结果为负数
return false;
} else {
// 如果minuend 的 [0,i]可以承载,则向低位借(向低位借,可以避免浪费)
// 此时minuend[i]为负数,表示欠债
minuend[i] -= subtrahend[i];
// 将当前阶位的欠债,转移到前面的低阶位上,注意转移时,欠债x2
minuend[i - 1] += minuend[i] << 1;
// 转移后,当前阶位的欠债变为0
minuend[i] = 0;
}
}
}
return true;
}
int main() {
int D;
cin >> D;
getchar();
vector<int> N;
getchar();
int Ni;
while (cin >> Ni) {
N.emplace_back(Ni);
if (getchar() != ',') break;
getchar();
}
// D值作为减数subtrahend
// 将D值转化为二进制形式,并且为了和N[]的阶位进行对应,这里将D的二进制进行了反转
vector<int> subtrahend = dec2bin(D);
// N值作为被减数minuend
vector<int> minuend(subtrahend.size(), 0);
// count记录N能承载几个D
int count = 0;
for (int i = 0; i < N.size(); i++) {
if (i >= subtrahend.size()) {
// N 中 R ~ subtrahend.length 阶的单个软盘就能承载一个D,因此这些软盘有几个,就能承载几个D
count += N[i];
N[i] = 0;
} else {
// N 中 0 ~ min(R, subtrahend.length) 阶的软盘需要多个阶组合才能满足一个D
minuend[i] = N[i];
}
}
// 进行二进制减法
while (binarySub(minuend, subtrahend)) {
count++;
}
for (int i = 0; i < min(subtrahend.size(), N.size()); i++) {
N[i] = minuend[i];
}
cout << count << endl;
cout << "[";
for (int i = 0; i < N.size(); i++) {
cout << N[i];
if (i < N.size() - 1) {
cout << ", ";
}
}
cout << "]";
return 0;
}