Как набрать нужную сумму из определенного количества монет?

Здравствуйте, есть известная задача о наборе сдачи из бесконечного количества монет определенного номинала. Недавно столкнулся с похожей задачей. Есть монеты известных номиналов, их ограниченное количество, нужно сказать, какое максимальное количество раз можно набрать определенную сумму из этого набора (соответственно, при нахождении одного набора, все взятые монеты вычитаются из общего количества). Допустим пусть a, b, c - номиналы монет, x, y, z - количество монет каждого номинала и e - сумма которую нужно набрать. Получается выражение
x * a + y * b + z * c = e.

Я думаю что можно попробовать применить бинарный поиск по ответу, но не понимаю, как лучше набирать нужные монеты и как убедиться что я получу наибольшее количество?
  • Вопрос задан
  • 1224 просмотра
Решения вопроса 5
mayton2019
@mayton2019
Bigdata Engineer
Задача по смыслу очень похожа на укладку рюкзака (Knapsack_problem)
https://en.wikipedia.org/wiki/Knapsack_problem
Ответ написан
Комментировать
wataru
@wataru Куратор тега Алгоритмы
Разработчик на С++, экс-олимпиадник.
Можно целочисленным линейным программированием решать. Бинплиском перебтрайте ответ (k), потом составляйте задачу, где у вас переменные: сколько монет каждого типа берём в каждый из k разменов. И еще будут неравенства, что каждый тип монет используется суммарно не больше, чем есть монет. Целевая функция - любая.

Еще можно чуть по-другому: находите переблром все варианты одного размена. Потом у вас переменные будут, сколько раз каждый из вариантов берете. Ограничения на общий расход каждой монеты, а целевая функция - сумма переменных. Тут нет бинплиска, но тоже ЦЛП.
Ответ написан
Комментировать
pickHabr
@pickHabr
Костыльных дел мастер
пусть a, b, c - номиналы монет, x, y, z - количество монет каждого номинала и e - сумма которую нужно набрать. Получается выражение x * a + y * b + z * c = e.

- это не правильное выражение (точнее оно будет верным только в частном случае, когда искомая сумма будет равна количеству всех имеющихся денег)

x[i] * a + y[j] * b + z[k] * c = e - вот это правильное

Что мы знаем из входных данных? мы знаем сколько у нас есть денег всего, это как раз x * a + y * b + z * c, пусть это будет N;
Дальше мы знаем сумму которую необходимо набрать это e;
Нам нужно понять сколько раз мы можем получить e из N, пусть это будет Q
N / e = M - количество способов получить e математически
Это нам не подходит так как нам надо учитывать номиналы, но теперь у нас есть ограничение
0 <= Q <= M
это к вопросу
как убедиться что я получу наибольшее количество


Дальше будет вариант решения задачи неоптимальным способом "как получится" (смотри статус профиля) более правильным вариантом будет почитать другие ответы на вопрос и составить собственный вариант решения задачи с учетом теории из линейного программирования

1. понять, какие номиналы будут участвовать в получение e (и есть ли они у нас в целом)
2. отсечь случай e === N
3. получить все комбинации для получения e
4. понять какие из них сочитаются между собой с учетом ограниченности монет
5. найти наибольшее количество сочитаемых

Дальше предоставляю говнокод на js который решает эту задачу как минимум в частных случаях (я проверил парочку вариантов входных параметров)

Оно

// Входные параметры

const a = 1;
const b = 2;
const c = 5;

const x = 3;
const y = 2;
const z = 2;

const e = 7;


// Отсекаем лишнее

let params = [];

if (getEntryNominal(c, e) >= 1) {
    params.push({
        'count': z,
        'nominal': c
    });
}

if (getEntryNominal(b, e) >= 1) {
    params.push({
        'count': y,
        'nominal': b
    });
}

if (getEntryNominal(a, e)  >= 1) {
    params.push({
        'count': x,
        'nominal': a
    });
}

let count = 0;
const N = getN(a, b, c, x, y, z);
if (N === e) {
    count = 1;
} else {
    count = getCombinationsAll(params, e)
}

console.log(count);


function getN(a, b, c, x, y, z) {
    return x * a + y * b + z * c;
}

function getEntryNominal(nominal, e) {
    return Math.floor(e / nominal);
}


function getCombinationsAll(params, e) {
    let Q = 0;

    let combinations = [];

    let maxLength = parseInt() + parseInt(params[1]?.count) + parseInt((params[2])?.count ?? 0);
    const iM = (params[0]?.count ?? 0);
    const jM = (params[1]?.count ?? 0);
    const kM = (params[2]?.count ?? 0);

    let i = iM;
    let j = jM;
    let k = kM;

    let a = (params[0]?.nominal ?? 0);
    let b = (params[1]?.nominal ?? 0);
    let c = (params[2]?.nominal ?? 0);

    // Вычисляем количество всех возможных комбинаций из которых получается e
    while (i + j + k > 0) {
	let bufE = getN((params[0]?.nominal ?? 0), (params[1]?.nominal ?? 0), (params[2]?.nominal ?? 0), i, j, k);

        if (bufE === e) {
            let obj = {};
            obj[a] = i;
            obj[b] = j;
            obj[c] = k;
            combinations.push(obj);
        }
        k--;
        if (k < 0) {
            j--;
            k = kM;
        }
        if (j < 0) {
            i--;
            j = jM;
        }

        if (i < 0) {
            i = 0 
        }
    }

    console.log(combinations);

    // составим список сочитаемых комбинаций
    let combComb = [];
    for (w = 0; w < combinations.length; w++) {
        i = iM;
        j = jM;
        k = kM;

        combComb[w] = [];

        for (v = 0; v < combinations.length; v++) {
            if (
                i - combinations[v][a] >= 0
                && j - combinations[v][b] >= 0
                && k - combinations[v][c] >= 0
            ) {
                i -= combinations[v][a];
                j -= combinations[v][b];
                k -= combinations[v][c];

                combComb[w].push(combinations[v]);
            }
        }
    }

    // ищем максимальное количество комбинаций
    for (w = 0; w < combComb.length; w++) {
        if (combComb[w].length > Q) {
            Q = combComb[w].length;
        }
    }

    console.log(combComb);

    return Q;
}

Ответ написан
Комментировать
Rsa97
@Rsa97
Для правильного вопроса надо знать половину ответа
В лоб - составить все возможные варианты набора определённой суммы, затем определить непересекающиеся группы наборов и для каждой группы рекурсивно перебирать все комбинации.

Скажем, номиналы 5, 4, 3, 2, необходимая сумма 8
Получаем наборы (5, 3), (4, 4), (4, 2, 2), (3, 3, 2), (2, 2, 2, 2).
Одна группа, в которую входят все наборы.
Номиналы 5, 4, 3, необходимая сумма 8
Получаем наборы (5, 3), (4, 4).
Две группы [(5, 3)] и [(4, 4)]

Для каждой группы.
Начиная с 0 смотрим, в цикле можем ли мы ещё раз взять первый набор. Если да, то рекурсивно смотрим, сколько раз можно набрать нужную сумму остальными наборами из группы из остатка монет. на каждом шаге цикла суммируем количество первых наборов и вернувшееся количество наборов из остатков. Ищем максимум этой суммы.
Пройдя по всем группам суммируем результаты, получаем общее число раз.
Ответ написан
Комментировать
ZERGeich
@ZERGeich
Если данные по номиналам и требуемой сумме изначально неизвестны, а вводятся пользователем, то мне в голову с первого захода придумывается только перебором:
N - требуемая сумма.
a[i] - массив номиналов.
1. N делим нацело на a[max] - если остаток больше нуля - продолжаем уменьшать i пока не получим ноль в остатке.
2. На следующей итерации max - уменьшаем на один и возвращаемся к первому пункту.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
LaRN
@LaRN
Senior Developer
А если взять вме монеты и положить в список.
Затем отсортировать по номиналу.
Затем методом двух указателей идти и набирать нужную сумму.
По идее когда указатели встретятся где-то в середине списка д.б. найдены n вариантов набора суммы.

Общее максимальное количество можно оценить взяв сумму номиналов всех монет и поделить на сумму, которую нужно набрать. Это для контроля результата.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы