Каков наиболее эффективный алгоритм для проверки, суммирует ли серия целых чисел до 0, назначая знаки? - PullRequest
0 голосов
/ 26 ноября 2018

Я хотел бы эффективно решить следующие вопросы:

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

Пример:

исходная последовательность: 1, 3, 5, 2, 1, 4

вывод: +5, -4, -3, +2, -1, + 1

Идеи:

Попробуйте каждую комбинацию одну за другой,Для 6 чисел, которые будут выглядеть примерно так (только знаки):

++++++
+++++-
++++-+
++++--
and so on...

Попробуйте сначала отсортировать последовательность.Присвойте + первому числу, затем вычитайте, пока не получите отрицательный результат, затем снова добавьте, пока не получите положительный.

first sort: 
5, 4, 3, 2, 1, 1
+5 (sum = 5) 
+5, -4 (sum = 1) 
+5, -4, -3 (sum = -2)
+5, -4, -3, +2 (sum = 0) 
+5, -4, -3, +2, -1 (sum = -1)
+5, -4, -3, +2, -1, +1 (sum = 0)

Есть ли лучший способ решить эту проблему?Второй имеет смысл или есть возможности, где это не сработает (при условии, что вы можете сложить seq в 0)?

Ответы [ 4 ]

0 голосов
/ 26 ноября 2018

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

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

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

Вот мой код, демонстрирующий возврат в javascript, исполняемый в браузере.

/** Returns 
  - false if it can't find a solution
  - An array of signs otherwise 
  */
const findZeroSum = function(problem) {

  const sumElement = (partial, sign, value) => {

    if (sign == '+')
      partial += value;
    else if (sign == '-')
      partial -= value;
    return partial;
  };


  const sortBySubElementAsc = (index) => (a, b) =>
    a[index] - b[index];

  const recursiveFindZeroSum = (partial, index, prob, sol) => {
    const sums = [];
    const signs = '+-';
    const finalElement = ((index + 1) >= prob.length);

    for (let i in signs) {
      let el = signs[i];
      let sum = sumElement(partial, el, prob[index]);

      if (finalElement && sum == 0) {
        // we found a solution!!
        sol[index] = el;
        return sol;
      }
      // store to explore later
      sums.push([el, sum, Math.abs(sum)]);

    }
    if (finalElement) return false;
    // order by the better partial solution
    // (the closest to zero)
    const sortedCandidates = sums.sort(sortBySubElementAsc(2));

    for (let i in sortedCandidates) {
      let el = sortedCandidates[i];
      sol[index] = el[0];
      // go down in the tree
      let partialSol = recursiveFindZeroSum(el[1], index + 1, problem, sol)
      if (partialSol !== false) {
        return partialSol;
      }
    }
    return false;

  };
  const sol = recursiveFindZeroSum(0, 0, problem, []);
  return sol;
  
};
// generate a solution, for testing
const genZeroSum = (start, end) => {
  const res = [];
  let sum = 0;
  for (let i = start; i < end; i++) {
    res.push(i);
    if (Math.random() > 0.5) {
      sum += i;
    } else {
      sum -= i;
    }
  }
  res.push(Math.abs(sum));
  return res;
};

tests = [
  [1, 1, 2, 5],
  [12, 1, 25, 5],
  [12, 12, 25, 1],
  genZeroSum(1, 20),
  genZeroSum(15, 40),
];

tests.forEach((d,i) => {
  console.log("Test "+i);
  console.log(d.join());
  let sol = findZeroSum(d);
  if (sol){
    sol = sol.join(' ');
  }
  console.log(sol);
});
0 голосов
/ 26 ноября 2018

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

bool recursion(int pos, int n, int sum, vector<int>&sequence) {
  if (pos == n) {
     if (sum == 0) return true;
     else return false;
   }
   bool resultTakingPositive =  recursion(pos + 1, n, sum + sequence[pos], sequence);
   bool resultTakingNegative =  recursion(pos + 1, n, sum - sequence[pos], sequence);
   if (resultTakingPositive || resultTakingNegative) return true;
   else return false;
}

Если существует всего n целых чисел, то это решение займет сложность времени O(2^n).Потому что в каждой позиции есть два варианта:

  • Take + ve значение при суммировании.
  • Take -ve значение при суммировании.

И,мы должны сделать выбор для каждого n целого числа.Итак, n умножение на 2 приводит к O (2 ^ n) временной сложности.

В случае вашей второй идеи:
Вы пытаетесь отсортироватьСначала выполните последовательность действий в порядке возрастания и присвойте знак + ve первому числу, затем вычитайте, пока не получите отрицательное число, затем снова добавьте, пока не получите положительное число.К сожалению, этот жадный подход не всегда работает.Например:
В последовательности: 5, 4, 4, 3, 2
Если мы попробуем этот подход, у нас будет: +5 -4 -4 +3 +2, что приводит к суммированию = 2.
Но мы можем сделать суммирование нулем, выполнив: +5 +4 -4 -3 -2.

Эффективный подход:
Мы можем использовать запоминание в вышеупомянутом рекурсивном решении с простой модификацией, чтобы позволить позитивную индексацию при выполнении запоминания со состояниями pos иsum.Это также называется динамическим программированием.Для этого максимально возможное значение pos * sum должно быть меньше, чтобы кэшировать их состояния в памяти, используя двумерный массив.Таким образом, сложность во времени и пространстве будет O(n * sum).Примером такого подхода с использованием кода c ++ будет:

#include<bits/stdc++.h>
using namespace std;

bool recursion(int pos, int n, int sum, vector<int>&sequence,int &baseSum,  vector< vector<int> >&dp) {
  if (pos == n) {
     if (sum == baseSum) return true;
     else return false;
  }
  if (dp[pos][sum] != -1) return dp[pos][sum];
  bool resultTakingPositive =  recursion(pos + 1, n, sum + sequence[pos], sequence, baseSum, dp);
  bool resultTakingNegative =  recursion(pos + 1, n, sum - sequence[pos], sequence, baseSum, dp);
  dp[pos][sum] = (resultTakingPositive || resultTakingNegative);
  return dp[pos][sum];
}

int main() {
  vector<int>sequence;
  int n, baseSum = 0;
  scanf("%d",&n);
  for (int i = 1; i <= n; i++) {
     int x;
     scanf("%d",&x);
     sequence.push_back(x);
     baseSum += x;
  }
  vector< vector<int> >dp(n, vector<int>(2*baseSum + 1, -1));
  cout<<recursion(0, n, baseSum, sequence, baseSum, dp)<<endl;
  return 0;
}

Теперь, если мы хотим отслеживать знаки, используемые для суммирования 0, мы можем сделать это путем анализа рекурсивных вызовов следующим образом:

#include<bits/stdc++.h>
using namespace std;

bool recursion(int pos, int n, int sum, vector<int>&sequence,int &baseSum, vector< vector<int> >&dp) {
  if (pos == n) {
     if (sum == baseSum) return true;
     else return false;
  }
  if (dp[pos][sum] != -1) return dp[pos][sum];
  bool resultTakingPositive =  recursion(pos + 1, n, sum + sequence[pos], sequence, baseSum, dp);
  bool resultTakingNegative =  recursion(pos + 1, n, sum - sequence[pos], sequence, baseSum, dp);
  dp[pos][sum] = (resultTakingPositive || resultTakingNegative);
  return dp[pos][sum];
}

void printSolution(int pos, int n, int sum, vector<int>&sequence,int &baseSum, vector< vector<int> >&dp) {
  if (pos == n) {
    cout<<endl;
    return;
  }
  bool resultTakingPositive =  recursion(pos + 1, n, sum + sequence[pos], sequence, baseSum, dp);
  if (resultTakingPositive == true) {
     cout<< "+ ";
     printSolution(pos + 1, n, sum + sequence[pos], sequence, baseSum, dp);
  } else {
    cout<< "- ";
    printSolution(pos + 1, n, sum - sequence[pos], sequence, baseSum, dp);
  }
}

int main() {
  vector<int>sequence;
  int n, baseSum = 0;
  scanf("%d",&n);
  for (int i = 1; i <= n; i++) {
     int x;
     scanf("%d",&x);
     sequence.push_back(x);
     baseSum += x;
  }
  vector< vector<int> >dp(n, vector<int>(2*baseSum + 1, -1));
  if (recursion(0, n, baseSum, sequence, baseSum, dp)) { // if possible to make sum 0 then
      printSolution(0, n, baseSum, sequence, baseSum, dp);
   }
   return 0;
}
0 голосов
/ 26 ноября 2018

Не существует «наиболее эффективного» алгоритма для этой задачи.

С теоретической точки зрения сложность в наихудшем случае не является полиномиальной, так что грубая сила (пробуя все знаки) приемлема.А для задач небольшого размера (скажем, менее 20 элементов) это может быть достаточно быстро.

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

0 голосов
/ 26 ноября 2018

Это subset sum problem.Вам необходимо вычислить сумму всех элементов массива, пусть at будет S.Тогда у вас должно быть подмножество, такое как сумма его равна S/2.Это известная проблема, и она решается dynamic programming.Вы могли прочитать об этом Алгоритм Суммы Подмножества

...