Разделить массив на K подмассивов с минимальной разницей
Автор вопроса: CaptainTrunkyОтветов: 3
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ:
Описанная проблема выглядит как задача из конкурса. Я не участвую ни в одном из них, я не знаю о каких-либо текущих соревнованиях, в которых может быть проблема. Если они есть, я закрою вопрос, чтобы оставаться честным!
У меня есть проблема: учитывая массив значений A и целое число K, разбить A ровно на K непересекающихся смежных подмассивов таким образом, чтобы разница между подмассивом с минимальной и максимальной суммами подмассива была минимальной. Разрешается вращать A на любое число в любом направлении.
У меня есть частично работающий код (ужасно некрасивый, мой плохой, но это не означает, что он будет качественным):
#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;
}
Идея здравая, но в коде есть некоторые проблемы. На мой взгляд, вы на самом деле не используете внешний цикл (смещение), поэтому вы определенно не правильно вращаете. Функция суммы включает оба конца, так что вы эффективно просматриваете подмассивы, которые перекрываются в своих конечных точках. Ваша оценка сложности неверна: я насчитал 5 вложенных циклов до n и один до k. Плюс функция суммы зацикливается, что в сумме приближает ее к O(KN^6). В противном случае это не выглядит слишком далеким от правильного (хотя для достижения O (KN ^ 4) может потребоваться некоторая работа). gus 17.02.2018
@гус Спасибо! Я решил некоторые проблемы, посмотрите обновленный пост. Однако это все еще не дает ожидаемых результатов. CaptainTrunky 18.02.2018
Идея следующая: считаем, что интервал минимальной суммы всегда начинается с 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;
}
Ответ 2 · рейтинг 1 · Apoorv
Решение без чередования:
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)
Для всех вращений в [0, 1,... N-1] вычислите минимальную сумму.
Общая временная сложность = O(n) * O(nlog(S-M)) = O(n^2*log(S-M эм>))
Ответ 3 · גלעד ברקן
Теперь, когда ваш код работает, вот альтернативный метод :)
Учтите, что для каждого 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 = 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