Количество способов разделить n объектов на k групп, чтобы ни одна группа не имела меньше объектов, чем ранее сформированные группы? - PullRequest
2 голосов
/ 06 октября 2019

Пример: n = 8, k = 4 Ответ: 5

[1,1,1,5], [1,1,2,4], [1,1,3,3], [1,2,2,3], [2,2,2,2]

Я думал о применении динамического программирования для подсчета количества способов, которыми 8 объектов можно разделить на 4 группы, но можно:Не понимаю, как отслеживать количество объектов в предыдущей группе.

Подход DP:

for(int j=0;j<=n;j++)
{
    for(int i=1;i<=k;i++)
    {
        if(j<i)
            continue;
        if(j==i)
            dp[j]=1;
        for(int k=1;k<i;k++)
        {
            dp[j]+=dp[k]*dp[j-k];
        }
    }
}

Пожалуйста, помогите с подходом. Я относительно новичок в DP.

Ответы [ 3 ]

2 голосов
/ 09 октября 2019

Они известны как разделы с ограниченным количеством частей . Идея повторения, равная количеству разделов, большая часть которых равна k (доказательство оставлено в виде короткого, интересного чтения), состоит в том, что если наименьшая часть раздела равна 1, мы добавили 1 квсе разбиения n - 1 на k - 1 частей (чтобы гарантировать, что наименьшая часть равна 1);и если наименьшая часть не равна 1, мы добавили 1 к каждой из k частей во всех разделах n - k на k частей (гарантируя, что каждая часть больше 1).

Вот прямое напоминание:

function f(n, k, memo={}){
  if (k == 0 && n == 0)
    return 1

  if (n <= 0 || k <= 0)
    return 0

  let key = String([n, k]) // Thanks to comment by user633183

  if (memo.hasOwnProperty(key))
    return memo[key]

  return memo[key] = f(n - k, k, memo) + f(n - 1, k - 1, memo)
}

console.time('time taken')
console.log(f(1000, 10))
console.timeEnd('time taken')

Здесь снизу вверх:

function f(n, k){
  let dp = new Array(n + 1)
  for (let i=0; i<n+1; i++)
    dp[i] = new Array(k + 1).fill(0)
  dp[0][0] = 1
  
  for (let i=1; i<=n; i++)
    for (let j=1; j<=Math.min(i, k); j++)
      dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1]

  return dp[n][k]
}

console.time('time taken')
console.log(f(1000, 10))
console.timeEnd('time taken')
1 голос
/ 08 октября 2019

Обновление

Хотя все приведенные ниже обсуждения по-прежнему полезны, ответ от גלעד ברקן предоставляет гораздо более хороший базовый алгоритм, который позволяет нам пропустить мой параметр min. (Я знал Я должен был это посмотреть!) Это понимание позволяет значительно улучшить производительность по сравнению с алгоритмом, использованным ниже.


Думайте о динамическом программировании (DP) как о простой оптимизациитехника, которая может ускорить определенные рекурсивные процедуры. Если ваши рекурсивные вызовы повторяются (как в случае с числами Фибоначчи), то сохранение их результатов может значительно ускорить вашу программу. Но основная логика все еще является рекурсивным вызовом. Итак, давайте сначала рекурсивно решим эту программу и посмотрим, где мы могли бы применить оптимизацию DP.

(8, 4) только с пятью решениями достаточно мало, что, даже если время является алгоритмически экспоненциальным, оно все еще, вероятно, управляемо. Давайте попробуем простую рекурсию. И сначала давайте построим вывод, а не посчитаем его, чтобы перепроверить, что мы все делаем правильно.

Эта версия основана на идее, что мы можем установить первое число в спискеотслеживая это значение как минимум для оставшихся элементов, а затем повторяя для оставшихся позиций. Наконец, мы попробуем еще раз с большим начальным числом. Таким образом, наряду с нашими n и k входами нам также потребуется сохранить параметр min, который мы начинаем с 1.

Вот одна из версий:

const f = (n, k, min = 1) => 
  k < 1 || n < k * min
    ? []
  : k == 1
    ? [[n]]
  : [
      ... f (n - min, k - 1, min) .map (xs => [min, ...xs]), 
      ... f (n, k, min + 1)
    ]

console .log (
  f (8, 4) //~> [[1, 1, 1, 5], [1, 1, 2, 4], [1, 1, 3, 3], [1, 2, 2, 3], [2, 2, 2, 2]]
)

(Вы не указали языковой тег; если этот синтаксис Javascript ES6 неясен, мы можем переписать его в другом стиле.)

После этогокажется правильным, мы можем написать более простую версию для подсчета результатов:

const f = (n, k, min = 1) => 
  k < 1 || n < k * min
    ? 0
  : k == 1
    ? 1
  : f (n - min, k - 1, min) + f (n, k, min + 1)

console .log (
  f (8, 4) //~> 5
)

Но если мы собираемся попробовать больший набор, скажем, f (1000, 10) (который, согласно проверке, должен быть 886745696653253 1 ), это может занять немного времени, чтобы рассчитать. Наш алгоритм скорее экспоненциальный. Так что для этого есть два способа динамического программирования. Наиболее очевидным является восходящий подход:

const f = (_n, _k, _min = 1) => {
  const cache = {}
  for (let n = 1; n <= _n; n ++) {
    for (let k = 1; k <= Math.min(_k, n); k++) {
      for (let min = n; min >= 0; min--) {
        cache [n] = cache[n] || {}
        cache [n] [k] = cache [n] [k] || {}
        cache [n] [k] [min] = 
          k < 1 || n < k * min
            ? 0
            : k == 1
               ? 1
               : cache [n - min] [k - 1] [min]  + cache [n] [k] [min + 1]
      }
    }
  }
  return cache [_n] [_k] [_min]
}

console.time('time taken')
console .log (
  f (1000, 10) //~> 886745696653253
)
console.timeEnd('time taken')

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

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

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

const memoize = (makeKey, fn) => {
  const cache = {}
  return (...args) => {
    const key = makeKey(...args)
    return cache[key] || (cache[key] = fn(...args))
  }
}

const makeKey = (n, k, min) => `${n}-${k}-${min}`        
        
const f = memoize(makeKey, (n, k, min = 1) => 
  k < 1 || n < k * min
    ? 0
  : k == 1
    ? 1
  : f (n - min, k - 1, min)  + f (n, k, min + 1)
)

console.time('time taken')
console .log (
  f (1000, 10) //~> 886745696653253
)
console.timeEnd('time taken')

memoize превращает функцию, которая вычисляет свои результаты при каждом вызове, в функцию, которая вычисляет их только при первом обнаружении определенного набора входных данных. Эта версия требует, чтобы вы предоставили дополнительную функцию, которая превращает аргументы в уникальный ключ. Есть и другие способы написания, но они немного уродливее. Здесь мы просто превращаем (8, 4, 1) в "8-4-1", а затем сохраняем результат под этим ключом. Там нет двусмысленности. В следующий раз, когда мы вызовем (8, 4, 1), уже вычисленный результат будет мгновенно возвращен из кэша.

Обратите внимание, что существует соблазн попробовать

const f = (...args) => {...}
const g = memoize(createKey, f)

BuЭто не работает, если рекурсивные вызовы в f указывают на f. И если они указывают на g, мы уже перепутали реализацию, и f больше не является автономной, поэтому для этого нет особых причин. Таким образом мы пишем это как memomize(createKey, (...args) => {...}). Расширенные методы, которые предлагают альтернативы , выходят за рамки обсуждения здесь.


Выбор между восходящим и нисходящим DP является сложным вопросом. В вышеприведенном случае вы увидите, что восходящая версия работает быстрее для данного ввода. Есть некоторые рекурсивные издержки при вызове дополнительных функций, и в некоторых случаях вы можете быть подвержены ограничениям глубины рекурсии. Но иногда это полностью компенсируется нисходящей техникой, вычисляющей только то, что вам нужно. Снизу вверх вычислит все меньшие входные данные (для некоторого определения «меньший»), чтобы найти ваше значение. Сверху вниз будут рассчитываться только те значения, которые необходимы для решения вашей проблемы.


1 Шутка! Я нашел значение только после использования динамического программирования.

0 голосов
/ 08 октября 2019

Из примера я предполагаю, что ни одна группа не может быть пустой. Также предполагается, что значение n, k <= 1000. </p>

Состояния dp будут remaining Objects и remaining Groups. f(remObject,remGroup) будет числом способов поместить remObject в remGroup, где ни у одной группы не будет меньше объектов, чем у ранее сформированных групп.

Мы рассмотрим 2 случая.

Если мыЕсли вы хотите поместить объект в самую левую группу, нам также нужно будет поместить объект и во все остальные группы. Поэтому мы должны убедиться, что remaining Objects >= remaining Groups. В этом случае мы добавим f(remObject - remGroup, remGroup) к нашему ответу.

Если мы больше не хотим помещать какой-либо объект в самую левую группу, мы добавим f(remObject,remGroup - 1) с нашим ответом.

И базовый случай будет, когда не осталось групп для рассмотрения и все объекты размещены.

Поскольку ни одна группа не может быть пустой, перед вызовом нашего dp мы поместим 1 объект во все k групп.

Посмотрите код для более подробной информации.

#define mxn 1003
#define i64 long long int
#define mod 1000000007

i64 dp[mxn][mxn];

i64 f(int remObject,int remGroup) {
        if(!remGroup) {
                if(!remObject)
                        return 1;
                return 0;
        }

        if(dp[remObject][remGroup] != -1)
                return dp[remObject][remGroup];

        i64 ans = 0;
        if(remObject >= remGroup)
                ans += f(remObject - remGroup, remGroup);
        ans += f(remObject,remGroup - 1);
        ans %= mod;

         return dp[remObject][remGroup] = ans;
}

int main()
{
        int t,n,k;
        memset(dp,-1,sizeof dp);
        cin >> t;
        while(t--) {
                cin >> n >> k;
                if(n < k)
                        cout << 0 << endl;
                else
                        cout << f(n-k,k) << endl;
        }
        return 0;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...