Найти минимальное количество элементов, сумма которых равна X из массива - PullRequest
1 голос
/ 25 января 2020

Недавно я столкнулся с вопросом на собеседовании, где вы должны найти минимально необходимые элементы из массива, который можно сложить вместе, чтобы сгенерировать X значение.

Например, дать:

[1, 9, 2, 5, 3, 10] и цель: 13, => это должно быть [10, 3]

  • Вы не можете использовать любое число вне массива. Так что [10, 9, 9, 2] и 20 -> [10, 10] недопустимы, но [9, 9, 2] совершенно нормально
  • Сложность по времени не имеет значения (но приветствуются более эффективные решения)
  • Любой допустимое число, включая отрицательные числа.

Я пытался сортировать предметы и брать с головы один за другим и некоторые другие вещи, но без удачи. Хотя я решил это не очень эффективным решением, и я опубликую его как ответ. Я ищу более эффективное решение.

Последнее, что я придумал, это алгоритм :

  1. Найти все подмножества с суммой целей.

  2. Найдите наименьшее подмножество.

Ответы [ 2 ]

3 голосов
/ 25 января 2020

Возврат

Лучшее решение, которое я могу придумать (с точки зрения сложности времени), - это алгоритм возврата.

Это очень похоже на грубую силу. В худшем случае он имеет одинаковую временную сложность грубой силы. Но это немного лучше, потому что он проверяет комбинации только там, где это имеет смысл.

Теория

Мы используем рекурсивную функцию visit, чтобы исследовать дерево комбинаций.

enter image description here

Каждая комбинация представлена ​​путем от root до одного листа.

Пока здесь ничто не отличается от грубой силы, верно?

Однако наша функция будет достаточно умна, чтобы прекратить исследовать ветку дерева, когда построенное частичное решение будет равно или больше целевого значения (в вашем случае - 13).

Эта небольшая вещь делает Backtraking лучше, чем грубая сила для некоторых входных данных.

В худшем случае обратный возврат будет медленным, как грубая сила.

Но есть проблема !

Спасибо @ MartinR за указание на то, что текущая идея не работает с отрицательными числами.

Например, учитывая этот массив [1, 1, 1, 1, 5, -1] и 4 в качестве целевого значения алгоритм m вернет [1, 1, 1, 1] как лучшее решение, не считая, что [5, -1] действительно лучше.

Управление отрицательными числами

Чтобы управлять отрицательными числами, я добавил следующую логику c.

Если целевое значение равно 0 или положительное , то входной массив сортируется в порядке по возрастанию (отрицательные числа будут поставлены первыми).

Так [1, 1, 1, 1, 5, -1] станет [-1, 1, 1, 1, 1, 5].

В противном случае, если цель отрицательна, входной массив будет отсортирован в порядке убывания.

Так [1, 1, 1, 1, 5, -1] станет [5, 1, 1, 1, 1, -1].

Кодирование 10

Решение Swift состоит из 4 частей

  1. SolutionHasNotExceededTarget typealias
  2. ArrayWithSum struct
  3. Функция smallestSubset(of:whereSumIs:)
  4. Функция visit(solution:unusedElms:target:solutionHasNotExceededTarget:)

1. SolutionHasNotExceededTarget

Мне нужно закрытие, чтобы проверить, превысило ли текущее решение цель.

Это зависит от знака цели.

Если цель неотрицательный, чем входной массив сортируется в порядке возрастания, и тогда текущее решение никогда не должно быть больше цели.

С другой стороны, если цель отрицательна, массив сортируется в порядке убывания, а затем текущее решение никогда не должно быть меньше цели.

Итак, резюмируем, что в случае отрицательной цели это закрытие будет

$0 > $1

Это означает, что если сумма текущих решение больше, чем цель, это нормально, потому что (будучи отрицательной целью) мы могли бы найти отрицательные числа позже, когда мы go глубже в дереве.

В противном случае, если цель не отрицательна, закрытие будет

$0 < $1

Это определение типа такого замыкания.

typealias SolutionHasNotExceededTarget = (Int, Int) -> Bool

2. ArrayWithSum

Код должен будет вычислять сумму всех целых чисел в массиве много раз. Эта операция имеет временную сложность O (m), где m - длина массива. Я не хочу тратить время на вычисление одного и того же значения несколько раз, поэтому я определю оболочку для Array of Int для хранения суммы его элементов.

struct ArrayWithSum: Comparable {

    static let empty = ArrayWithSum([])
    let array: [Int]
    let sum: Int

    init(_ array: [Int]) {
        self.array = array
        self.sum = array.reduce(0, +)
    }

    private init(arrayWithSum: ArrayWithSum, elm: Int) {
        self.array = arrayWithSum.array + [elm]
        self.sum = arrayWithSum.sum + elm
    }

    func appending(elm: Int) -> ArrayWithSum {
        return ArrayWithSum(arrayWithSum: self, elm: elm)
    }

    static func < (lhs: ArrayWithSum, rhs: ArrayWithSum) -> Bool {
        lhs.array.count < rhs.array.count
    }

}

Как вы можете видеть, оболочка соответствует Comparable, что позволяет легко сравнивать 2 решения при поиске лучшего.

3. smallestSubset(of:whereSumIs:)

Эта функция подготовит данные для функции vist.

func smallestSubset(of nums: [Int], whereSumIs target: Int) -> [Int]? {

    let sorting: SolutionHasNotExceededTarget = target > 0
        ? { $0 < $1 }
        : { $0 > $1 }

    let sortedNums = nums.sorted(by: sorting)

    return visit(solution: .empty,
                 unusedElms: sortedNums,
                 target: target,
                 solutionHasNotExceededTarget: sorting)?.array
}

Сортирует массив в порядке возрастания, если цель не отрицательна. И в порядке убывания, если цель отрицательная.

4. visit(solution:unusedElms:target:solutionHasNotExceededTarget:)

Наконец, функция visit, которая применяет логику возврата c, описанную ранее.

func visit(solution: ArrayWithSum,
           unusedElms: [Int],
           target: Int,
           solutionHasNotExceededTarget: SolutionHasNotExceededTarget) -> ArrayWithSum? {

    if solution.sum == target {
        return solution
    }

    guard solutionHasNotExceededTarget(solution.sum, target) else {
        return nil
    }


    return unusedElms
        .enumerated()
        .map { (offset, elm) in
            var unusedElms = unusedElms
            unusedElms.remove(at: offset)
            return visit(solution: solution.appending(elm: elm),
                         unusedElms: unusedElms,
                         target: target,
                         solutionHasNotExceededTarget: solutionHasNotExceededTarget)
        }
        .compactMap { $0 }
        .min()
}

Test

Давайте запустим несколько тестов

smallestSubset(of: [1, 9, 2, 5, 3, 10], whereSumIs: 13)
> [3, 10]

smallestSubset(of: [1, 1, 1, 1, 5, -1], whereSumIs: 4)
> [-1, 5]

smallestSubset(of: [-1, 2, 10, 1, -1, -3, 5, -15], whereSumIs: -5)
> [10, -15]

smallestSubset(of: [-50, 2, 10, 1, -1, -3, 5, -5], whereSumIs: -5)
> [-5]

smallestSubset(of: [10, 9, 9, 2], whereSumIs: 20)
> [2, 9, 9]

Пространственная сложность

В отношении памяти, в худшем случае у нас будет столько открытых рекурсивных вызовов, сколько и высоты дерева.

Высота трех равна число элементов во входном массиве, так что.

Кроме того, рекурсивному вызову rach необходимо пространство пропорционально длине входного массива, поэтому.

Сложность пространства = O (n) * O (n) = O (n ^ 2)

Где n - количество элементов в входной массив.

Сложность времени

Как уже говорилось ранее, в худшем случае функция будет проверять каждую возможную комбинацию. Другими словами, каждый узел дерева будет посещен.

Сложность времени: O (2 ^ n)

Где n - это число элементы во входном массиве.

0 голосов
/ 25 января 2020

Сначала мы должны найти все подмножества массива:

extension Array {
    var allSubsets: [[Element]] {
        guard count > 0 else { return [[]] }

        let tail = Array(self[1..<endIndex]
        let head = self[0]
        let withoutHead = tail.allSubsets

        let withHead = withoutHead.map { $0 + [head] }

        return withHead + withoutHead
    }
}

Затем мы можем отфильтровать все подмножества, чтобы их сумма равнялась цели, например:

subsets.filter { $0.reduce(0) { $0 + $1 } == goal }

И, наконец, найдите наименьшее подмножество по его количеству:

validSubsets.reduce(array) { $0.count < $1.count ? $0 : $1 }

Оберните его в функцию следующим образом:

func minimumElements(in array: [Int], goal: Int) -> [Int] {
    let subsets = array.allSubsets

    let validSubsets = subsets.filter { subset in
        subset.reduce(0) { $0 + $1 } == goal
    }

    return validSubsets.reduce(array) { $0.count < $1.count ? $0 : $1 }
}

Обратите внимание, что I думаю это не очень эффективно, и было бы неплохо, если бы кто-то мог рассчитать сложность времени, но, поскольку в вопросе упоминалось, что сложность не имеет значения, это правильное решение.

...