Разделить массив на K подмассивов с минимальной разницей

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ:

Описанная проблема выглядит как задача из конкурса. Я не участвую ни в одном из них, я не знаю о каких-либо текущих соревнованиях, в которых может быть проблема. Если они есть, я закрою вопрос, чтобы оставаться честным!

У меня есть проблема: учитывая массив значений A и целое число K, разбить A ровно на K непересекающихся смежных подмассивов таким образом, чтобы разница между подмассивом с минимальной и максимальной суммами подмассива была минимальной. Разрешается вращать A на любое число в любом направлении.

Рассмотрим пример:

Вход: А = [5 1 1 1 3 2], К = 3

Вывод: [5][1 1 1][3 2], максимальная сумма = 5, минимальная сумма = 3, результат = 2

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

#include <climits>
#include <cstdio>
#include <cstring>

const int max_n = 50;
const int max_k = 20;

int deps[max_n];

int max (int x, int y) {
  return x > y ? x : y;
}

int min (int x, int y) {
  return x < y ? x : y;
}

int sum (int a[], int start, int end) {
  int res = 0;
  for (int i = start; i <= end; ++i) res += a[i];

  return res;
}

int k_partitioning(int k, int n, int deps[]) {
  int res = INT_MAX;
  // consider all possible rotations/shifts
  for(int offset = 0; offset < n; ++offset) {
    for(int l_min = 0; l_min < n; ++l_min) {
      for(int r_min = l_min; r_min < n; ++r_min) {
        // check minimal sum subarray
        int min_sum = sum (deps, l_min, r_min);

        int dp[k][n];
        for (int s = 0; s < k; ++s) {
          for (int q = 0; q < n; ++q) {
            dp[s][q] = 0;
          }
        }
        // assuming that current sum is a target sum
        dp[0][r_min-l_min] = min_sum;

        for(int p = 1; p < k; ++p) {
          for(int l_max = 0; l_max < n; ++l_max) {
            for(int r_max = 0; r_max < n; ++r_max) {
              int max_sum = sum(deps, l_max, r_max);

              if (max_sum >= min_sum) dp[p][r_max] = max(dp[p-1][l_max], max_sum);
            } // l_maxs
          } // r_maxs
        } // partitions
        // printing dp

        // skip incorrect partitioning, when not all K partitions were used
        if (dp[k-1][n-1] == 0) continue;

        // update difference
        res = min (res, dp[k-1][n-1] - min_sum);
      } // end min sum seg
    } // start min sum seg
    //break;
  } // cuts
  return res;
}

int main(int argc, char* argv[]) {
  int k = 0;
  scanf("%d", &k);

  int n = 0;
  scanf("%d", &n);

  for (int i = 0; i < n; ++i) {
    scanf("%d", &deps[i]);
  }

  printf ("%d\n", k_partitioning(k, n, deps));

  return 0;
}

Идея проста: предположим, что текущий раздел имеет минимальную сумму, перечислите все возможные максимальные разделы, настройте динамическое программирование для получения максимальной суммы с минимальным значением, проверьте разницу. Общая сложность: O(K*N^4).

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

Неудачный тест, например:

N = 4, K = 2, A = [6 13 10 2]

ОБНОВИТЬ

Эта версия должна исправить некоторые предыдущие проблемы. Во-первых, он убирает ненужный цикл по «смещениям» и добавляет просто поворот массива в конце цикла l_min. Во-вторых, я заметил, что dp нельзя инициализировать 0 - это задача минимизации, поэтому его нужно инициализировать каким-то большим значением (зависит от констант задачи, max_value здесь уже вне области значений). Наконец, интервалы больше не должны перекрываться - каждая сумма исключает левый конец интервала. Тем не менее, это все еще не дает ожидаемых результатов.

#include <climits>
#include <cstdio>
#include <cstring>

const int max_value = 200000;
const int max_n = 50;
const int max_k = 20;

int deps[max_n];

int max (int x, int y) {
  return x > y ? x : y;
}

int min (int x, int y) {
  return x < y ? x : y;
}

int sum (int a[], int start, int end) {
  int res = 0;
  for (int i = start; i <= end; ++i) res += a[i];

  return res;
}

int k_partitioning(int k, int n, int deps[]) {
  int res = max_value;

  for(int l_min = 0; l_min < n; ++l_min) {
    for(int r_min = l_min; r_min < n; ++r_min) {
      int min_sum = sum (deps, l_min+1, r_min);

      int dp[k][n];
      for (int s = 0; s < k; ++s) {
        for (int q = 0; q < n; ++q) {
          dp[s][q] = max_value;
        }
      }
      // assuming that current sum is a target sum
      dp[0][r_min-l_min] = min_sum;

      for(int p = 1; p < k; ++p) {
        for(int l_max = 0; l_max < n; ++l_max) {
          for(int r_max = l_max; r_max < n; ++r_max) {
            int max_sum = sum(deps, l_max+1, r_max);

            if (max_sum >= min_sum) dp[p][r_max] = max(dp[p-1][l_max], max_sum);
          } // l_maxs
        } // r_maxs
      } // partitions

      // skip incorrect partitioning, when not all K partitions were used
      if (dp[k-1][n-1] == max_value) continue;

      // update difference
      res = min (res, dp[k-1][n-1] - min_sum);
    } // end min sum seg

    // rotate an array to consider different starting points
    int tmp[n];
    for (int i = 0; i < n; ++i) {
      int new_idx = i + n + 1;

      tmp[new_idx % n] = deps[i];
    }

    for(int i = 0; i < n; ++i) deps[i] = tmp[i];
  } // start min sum seg

  return res;
}

int main(int argc, char* argv[]) {
  int k = 0;
  scanf("%d", &k);

  int n = 0;
  scanf("%d", &n);

  for (int i = 0; i < n; ++i) {
    scanf("%d", &deps[i]);
  }

  printf ("%d\n", k_partitioning(k, n, deps));

  return 0;
}

person CaptainTrunky    schedule 17.02.2018    source источник
comment
Идея здравая, но в коде есть некоторые проблемы. На мой взгляд, вы на самом деле не используете внешний цикл (смещение), поэтому вы определенно не правильно вращаете. Функция суммы включает оба конца, так что вы эффективно просматриваете подмассивы, которые перекрываются в своих конечных точках. Ваша оценка сложности неверна: я насчитал 5 вложенных циклов до n и один до k. Плюс функция суммы зацикливается, что в сумме приближает ее к O(KN^6). В противном случае это не выглядит слишком далеким от правильного (хотя для достижения O (KN ^ 4) может потребоваться некоторая работа).   -  person gus    schedule 17.02.2018
comment
@гус Спасибо! Я решил некоторые проблемы, посмотрите обновленный пост. Однако это все еще не дает ожидаемых результатов.   -  person CaptainTrunky    schedule 18.02.2018


Ответы (3)


Хорошо, я думаю, что я сделал это!

Идея следующая: считаем, что интервал минимальной суммы всегда начинается с 0. Затем начинаем перечислять интервалы максимальной суммы, начиная с правой границы минимального интервала. Мы строим задачу DP для текущего максимального интервала, чтобы определить минимальную максимальную сумму. После этого вы обновляете результат и вращаете массив на единицу.

Мой код не идеален в том смысле, что я вычисляю текущие суммы на каждой итерации. Их можно предварительно вычислить и каждый раз просто индексировать.

В этом коде могут быть некоторые ошибки, но он проходит все тесты, которые у меня есть.

#include <climits>
#include <cstdio>
#include <cstring>

const int max_value = 200000;
const int max_n = 50;
const int max_k = 20;

int deps[max_n];

int max (int x, int y) {
  return x > y ? x : y;
}

int min (int x, int y) {
  return x < y ? x : y;
}

int sum (int a[], int start, int end) {
  int res = 0;

  for (int i = start; i <= end; ++i) res += a[i];

  return res;
}

int k_partitioning(int k, int n, int deps[]) {
  int res = max_value;
  for(int offset = 0; offset < n; ++offset) {
    int l_min = 0;
    for(int r_min = l_min; r_min < n; ++r_min) {
      int min_sum = sum (deps, l_min, r_min);

      int dp[k][n];
      for (int s = 0; s < k; ++s) {
        for (int q = 0; q < n; ++q) {
          dp[s][q] = max_value;
        }
      }
      // assuming that current sum is a target sum
      dp[0][r_min-l_min] = min_sum;

      for(int p = 1; p < k; ++p) {
        for(int l_max = r_min; l_max < n; ++l_max) {
          for(int r_max = l_max; r_max < n; ++r_max) {
            int max_sum = sum(deps, l_max+1, r_max);

            if (max_sum >= min_sum) {
              dp[p][r_max] = min(dp[p][r_max], max(dp[p-1][l_max], max_sum));
            }

          } // l_maxs
        } // r_maxs
      } // partitions

      // skip incorrect partitioning, when not all K partitions were used
      if (dp[k-1][n-1] == max_value) continue;

      // update difference
      res = min (res, dp[k-1][n-1] - min_sum);
    } // end min sum seg
    int tmp[n];
    for (int i = 0; i < n; ++i) {
      int new_idx = i + n - 1;

      tmp[new_idx % n] = deps[i];
    }

    for(int i = 0; i < n; ++i) deps[i] = tmp[i];

  } // start min sum seg
  return res;
}

int main(int argc, char* argv[]) {
  int k = 0;
  scanf("%d", &k);

  int n = 0;
  scanf("%d", &n);

  for (int i = 0; i < n; ++i) {
    scanf("%d", &deps[i]);
  }

  printf ("%d\n", k_partitioning(k, n, deps));

  return 0;
}
person CaptainTrunky    schedule 18.02.2018
comment
Привет @captaintrunky, могу я узнать логику решения. Я работал над этим, но терял его большую часть времени. Спасибо.. - person anupamD; 02.09.2018

Решение без чередования:

  • 1) Вычислить максимальное M и общее S массива - O(n)
  • 2) Пусть существует функция F(P), которая возвращает True, если возможно получить сумму P или меньше с оставшимися k (>= 0) разделами.
  • 3) Выполните бинарный поиск в диапазоне (M, S), используя F. - O(log(S-M))
  • 4) Логика F: наполняйте ведро, пока оно не превысит S/K. Затем переходите к следующему ведру. Если еще остались элементы и не осталось сегментов, то ответ неверен - O(n)

    Временная сложность = O(n) + O(n) * (log(S-M)) = O(n*log(S-M< /эм>))

Решение с чередованием:

Для всех вращений в [0, 1,... N-1] вычислите минимальную сумму.

Общая временная сложность = O(n) * O(nlog(S-M)) = O(n^2*log(S-M))

person Apoorv    schedule 28.10.2018

Теперь, когда ваш код работает, вот альтернативный метод :)

Учтите, что для каждого k мы можем соединить сумму, растущую от A[i] влево (sum A[i-j..i]), со всеми доступными интервалами, записанными для f(k-1, i-j-1), и обновить их - для каждого интервала (low, high), если сумма больше high, то new_interval = (low, sum) и если сумма меньше low, затем new_interval = (sum, high); в противном случае интервал остается прежним. Например,

i:  0 1 2 3 4 5
A: [5 1 1 1 3 2]

k = 3
i = 3, j = 0
The ordered intervals available for f(3-1, 3-0-1) = f(2,2) are:
  (2,5), (1,6) // These were the sums, (A[1..2], A[0]) and (A[2], A[0..1])
Sum = A[3..3-0] = 1
Update intervals: (2,5) -> (1,5)
                  (1,6) -> (1,6) no change

Теперь мы можем сделать эту итерацию намного более эффективной, распознав и сократив интервалы во время предыдущего k раунда.

Смотреть:

A: [5 1 1 1 3 2]

K = 1:

  N = 0..5; Intervals: (5,5), (6,6), (7,7), (8,8), (11,11), (13,13)

K = 2:

  N = 0: Intervals: N/A

  N = 1: Intervals: (1,5)

  N = 2: (1,6), (2,5)

    Prune: remove (1,6) since any sum <= 1 would be better paired with (2,5)
           and any sum >= 6 would be better paired with (2,5)

  N = 3: (1,7), (2,6), (3,5)

    Prune: remove (2,6) and (1,7)

  N = 4: (3,8), (4,7), (5,6), (5,6)

    Prune: remove (3,8) and (4,7)

  N = 5: (2,11), (5,8), (6,7)

    Prune: remove (2,11) and (5,8)

Для k = 2 у нас осталась следующая сокращенная запись:

{
  k: 2,
  n: {
    1: (1,5),
    2: (2,5),
    3: (3,5),
    4: (5,6),
    5: (6,7)
  }
}

Мы сократили итерацию k = 3 из списка n choose 2 возможных разбиений до n релевантных разбиений!

Общий алгоритм, примененный к k = 3:

for k' = 1 to k
  for sum A[i-j..i], for i <- [k'-1..n], j <- [0..i-k'+1]:
    for interval in record[k'-1][i-j-1]: // records are for [k'][n']
      update interval
  prune intervals in k'

k' = 3
  i = 2
    sum = 1, record[2][1] = (1,5) -> no change

  i = 3
    // sums are accumulating right to left starting from A[i]
    sum = 1, record[2][2] = (2,5) -> (1,5)
    sum = 2, record[2][1] = (1,5) -> no change

  i = 4
    sum = 3, record[2][3] = (3,5) -> no change
    sum = 4, record[2][2] = (2,5) -> no change
    sum = 5, record[2][1] = (1,5) -> no change

  i = 5
    sum = 2, record[2][4] = (5,6) -> (2,6)
    sum = 5, record[2][3] = (3,5) -> no change
    sum = 6, record[2][2] = (2,5) -> (2,6)
    sum = 7, record[2][1] = (1,5) -> (1,7)

Ответ: 5 в паре с record[2][3] = (3,5), что дает обновленный интервал (3,5). Я оставлю логику обрезки для читателя. Если мы хотим продолжить, вот сокращенный список для k = 3

{
  k: 3
  n: {
    2: (1,5), 
    3: (1,5),
    4: (3,5),
    5: (3,5)
  }
}
person גלעד ברקן    schedule 19.02.2018