Поскольку общее количество стихов и количество дней постоянно, вы хотите минимизировать
sum (avg verse count - verse count of day i)^2
i
avg verse count
- это константа и просто общее количество стихов, деленное на количество дней.
Эта проблема может быть решена с помощью динамической программы в течение нескольких дней.Давайте построим функцию частичного решения f(days, paragraph)
, которая дает нам минимальную сумму квадратов для распределения абзацев от 0
до paragraph
в течение days
дней.Нас интересует последнее значение этой функции.
Мы можем строить функцию постепенно.Вычислить f(1, p)
для любого p
просто, так как нам просто нужно вычислить разницу между средним и квадратом.Затем для всех остальных дней мы можем вычислить
f(d, p) = min f(d - 1, i) + (avg verse count - sum verse count of paragraph j)^2
i<p j:i+1..p
Это означает, что мы проверяем решения на один день меньше и заполняем текущий день абзацами между заключительным абзацем предыдущего дня и p
.Пока мы вычисляем эту функцию, мы сохраняем указатель на выбранный минимальный элемент (как обычно для динамической программы).Когда мы закончим вычисление всей функции, мы просто следуем указателям назад к началу, что даст нам разбиение.
Алгоритм имеет время выполнения O(d * p^2)
, где d
- числодней и p
- количество абзацев.
Пример кода
Вот пример кода C #, который реализует описанный выше алгоритм:
struct Entry
{
public double minCost;
public int predecessor;
}
public static void Main()
{
//input data
int[] versesPerParagraph = { 5, 6, 3, 10, 8, 4, 2, 6, 3, 10, 3, 2, 5, 10 };
int days = 7;
//calculate constants
double avgVerses = (double)versesPerParagraph.Sum() / days;
//set up DP table (f(d,p))
int paragraphs = versesPerParagraph.Length;
Entry[,] dp = new Entry[days, paragraphs];
//initialize table
int verseCount = 0;
for(int p = 0; p < paragraphs; ++p)
{
verseCount += versesPerParagraph[p];
double diff = avgVerses - verseCount;
dp[0, p].minCost = diff * diff;
dp[0, p].predecessor = -1;
}
//run dynamic program
for(int d = 1; d < days; ++d)
{
for(int p = d; p < paragraphs; ++p)
{
verseCount = 0;
dp[d, p].minCost = double.MaxValue;
for(int i = p; i >= d; --i)
{
verseCount += versesPerParagraph[i];
double diff = avgVerses - verseCount;
double cost = dp[d - 1, i - 1].minCost + diff * diff;
if(cost < dp[d, p].minCost)
{
dp[d, p].minCost = cost;
dp[d, p].predecessor = i - 1;
}
}
}
}
//reconstruct the partitioning
{
int p = paragraphs - 1;
for (int d = days - 1; d >= 0; --d)
{
int predecessor = dp[d, p].predecessor;
//calculate number of verses, just to show them
verseCount = 0;
for (int i = predecessor + 1; i <= p; ++i)
verseCount += versesPerParagraph[i];
Console.WriteLine($"Day {d} ranges from paragraph {predecessor + 1} to {p} and has {verseCount} verses.");
p = predecessor;
}
}
}
Вывод:
Day 6 ranges from paragraph 13 to 13 and has 10 verses.
Day 5 ranges from paragraph 10 to 12 and has 10 verses.
Day 4 ranges from paragraph 9 to 9 and has 10 verses.
Day 3 ranges from paragraph 6 to 8 and has 11 verses.
Day 2 ranges from paragraph 4 to 5 and has 12 verses.
Day 1 ranges from paragraph 2 to 3 and has 13 verses.
Day 0 ranges from paragraph 0 to 1 and has 11 verses.
Это разбиение дает стандартное отклонение 1.15
.