Извлечение предметов из неравномерно распределенного набора - PullRequest
9 голосов
/ 05 января 2012

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

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

Пример с пулом из 5 вопросов (и нужно удалить 3):

  • игрок А видел вопросы 1, 3 и 5
  • игрок Б видел вопросы 1 и 4
  • игрок C видел вопросы 2 и 4

Я думал об удалении вопросов, которые видел лучший игрок, но позиция изменилась бы. Следуя приведенному выше примеру, игроку А осталось сыграть только 2 вопроса (2 и 4). Однако, если я уберу 1, 3 и 5, ситуация будет такой:

  • игрок А может играть на вопросы 2 и 4
  • игрок B может сыграть вопрос 2
  • игрок C не может играть ничего, потому что 1,3,5 удалены, и он уже видел 2 и 4.

Оценка для этого решения равна нулю, т. Е. У игрока с наименьшим количеством доступных вопросов есть ноль доступных вопросов для игры.

В этом случае было бы лучше удалить 1, 3 и 4, давая:

  • игрок А может сыграть вопрос 2
  • игрок B может играть на вопросы 2 и 5
  • игрок C может сыграть вопрос 5

Счет для этого решения один, потому что у двух игроков с наименьшим количеством доступных вопросов есть один доступный вопрос.

Если бы размер данных был небольшим, я мог бы перебить решение проблемы. Тем не менее, у меня есть сотни игроков и вопросов, поэтому я ищу какой-нибудь алгоритм для решения этой проблемы.

Ответы [ 10 ]

4 голосов
/ 06 января 2012

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

Вы можете использовать такой алгоритм для решения проблемы - можете ли вы выбрать не более T вопросов, чтобы у каждого пользователя был хотя бы один вопрос?Я думаю, что это http://en.wikipedia.org/wiki/Set_cover,, и я думаю, что решение вашей проблемы в целом позволяет решить покрытие набора, поэтому я думаю, что оно является NP-полным.

По крайней мере, имеется линейное программирование.Свяжите каждый вопрос с переменной Qi в диапазоне 0 <= Qi <= 1. Выбор вопросов Qi так, чтобы у каждого пользователя было по крайней мере X доступных вопросов, равен ограничению SUM Uij Qj> = X, которое является линейным по Qj и X,так что вы можете максимизировать для целевой функции X с линейными переменными X и Qj.К сожалению, результат не должен давать вам целое число Qj - рассмотрим, например, случай, когда все возможные пары вопросов связаны с каким-либо пользователем, и вы хотите, чтобы каждый пользователь мог ответить хотя бы на 1 вопрос, используя не более половины вопросов.Оптимальным решением является Qi = 1/2 для всех i.

(Но, учитывая ослабление линейного программирования, вы можете использовать его в качестве границы в http://en.wikipedia.org/wiki/Branch_and_bound).

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

2 голосов
/ 16 января 2012

Я предлагаю несколько оптимизаций, основанных на идее, что вы действительно хотите максимизировать количество невидимых вопросов для игрока с минимальным количеством вопросов, и мне все равно, если есть 1 игрок с минимальным количеством вопросов или 10000 игроков с таким же количеством вопросов.

Шаг 1. Найдите игрока с минимальным количеством невидимых вопросов (в вашем примере это будет игрок А). Назовите этого игрока p.

Шаг 2: Найдите всех игроков с количеством вопросов, не виденных игроком p, в пределах 30. Назовите этот набор P. P - единственные игроки, которых нужно рассмотреть, так как удаление 30 невидимых вопросов от любого другого игрока все равно оставит им больше невидимых вопросов, чем у игрока p, и, таким образом, игроку p будет еще хуже.

Шаг 3: Найдите пересечение всех наборов проблем, замеченных игроками в P, вы можете удалить все проблемы в этом наборе, надеясь снизить количество ваших проблем с 30 до некоторого меньшего числа, которые мы назовем r. r <= 30 </p>

Шаг 4. Найдите объединение всех наборов проблем, которые видели игроки в P, назовите этот набор U. Если размер U равен <= r, все готово, удалите все проблемы в U, а затем удалите оставшиеся проблемы. произвольно из вашего набора всех проблем игрок p потеряет r - размер U и останется с наименьшим количеством невидимых проблем, но это лучшее, что вы можете сделать. </p>

Теперь у вас осталась исходная проблема, но, вероятно, с гораздо меньшими сетами.
Ваш набор проблем - U, ваш плеер - P, и вы должны удалить r проблем.

Метод грубой силы требует времени (размер (U) выберите r) * размер (P). Если эти цифры являются разумными, вы можете просто перебор. Этот подход состоит в том, чтобы выбрать каждый набор r задач из U и сравнить его со всеми игроками в P.

Поскольку ваша задача, по-видимому, является NP-Complete, лучшее, на что вы можете надеяться, это приближение. Самый простой способ сделать это - установить максимальное количество попыток, затем случайным образом выбрать и оценить наборы проблем, которые нужно удалить. Таким образом, функция для выполнения U выбора r случайным образом становится необходимой. Это можно сделать вовремя O (r), (На самом деле, я ответил, как это сделать сегодня раньше!)

Выберите N случайных элементов из списка в C #

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

2 голосов
/ 14 января 2012

Для полноты темы приведем простой жадный апроксимативный подход.

Поместите решенные вопросы в ранее обсужденную матричную форму:

Q0    X
Q1  XX
Q2    X
Q3  X  
Q4   XX
    223

Сортировка по количеству вопросоврешено:

Q0  X  
Q1   XX
Q2  X  
Q3    X
Q4  XX 
    322

Вычеркните вопрос с наибольшим количеством X с среди игроков с большинством решенных проблем.(Это гарантированно уменьшит наш показатель, если что-то есть):

=======
Q1   XX
Q2  X  
Q3    X
Q4  XX 
    222

Сортировка еще раз:

=======
Q1   XX
Q2  X  
Q3    X
Q4  XX 
    222

Удар еще раз:

=======
=======
Q2  X  
Q3    X
Q4  XX 
    211

Сортировка снова:

=======
=======
Q2  X  
Q3    X
Q4  XX 
    211

Ударь еще раз:

=======
=======
Q2  X  
Q3    X
=======
    101

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

Это не оптимально, как видно из этого примера с двумя ударами:

Q0 X     
Q1      X
Q2 XXX
Q3    XXX
Q4  XXXX
Q5 222222

Здесь жадный подход уберет Q5 и Q2 (или Q3) вместо Q2 и Q3, что было бы оптимальным для нашей меры.

1 голос
/ 13 января 2012

Модели линейного программирования.

Вариант 1.

Sum(Uij * Qj) - Sum(Dij * Xj) + 0     =  0 (for each i)
0             + Sum(Dij * Xj) - Score >= 0 (for each i)
Sum(Qj) = (Number of questions - 30)
Maximize(Score)

Uij равно 1, если пользователь i не видел вопроса j, в противном случае это 0

Dij является элементом единичной матрицы (Dij=1, если i=j, в противном случае это 0)

Xj является вспомогательной переменной (одна для каждого пользователя)

Вариант 2.

Sum(Uij * Qj) >= Score (for each i)
Sum(Qj) = (Number of questions - 30)
No objective function, just check feasibility

В этом случае проблема LP проще, но Score должна определяться двоичным и линейным поиском. Установите текущий диапазон на [0 .. наименьшее количество невидимых вопросов для пользователя], установите Score на середину диапазона, примените алгоритм целочисленного LP (с небольшим ограничением по времени). Если решение не найдено, установите диапазон на [begin .. Score], в противном случае установите его на [Score .. end] и продолжите бинарный поиск.

(опционально) использовать бинарный поиск, чтобы определить верхнюю границу для точного решения Score.

Начиная с лучшего Score, найденного при бинарном поиске, применить алгоритм целочисленного LP с Score, увеличенным на 1, 2, ... (и ограничение времени вычислений по мере необходимости). В конце вы получите либо точное решение, либо хорошее приближение.

Вот пример кода на C для GNU GLPK (для варианта 1):

#include <stdio.h>
#include <stdlib.h>
#include <glpk.h>

int main(void)
{
  int ind[3000];
  double val[3000];
  int row;
  int col;
  glp_prob *lp;

  // Parameters
  int users = 120;
  int questions = 10000;
  int questions2 = questions - 30;
  int time = 30; // sec.

  // Create GLPK problem
  lp = glp_create_prob();
  glp_set_prob_name(lp, "questions");
  glp_set_obj_dir(lp, GLP_MAX);

  // Configure rows
  glp_add_rows(lp, users*2 + 1);
  for (row = 1; row <= users; ++row)
  {
    glp_set_row_bnds(lp, row, GLP_FX, 0.0, 0.0);
    glp_set_row_bnds(lp, row + users, GLP_LO, 0.0, 0.0);
  }
  glp_set_row_bnds(lp, users*2 + 1, GLP_FX, questions2, questions2);

  // Configure columns
  glp_add_cols(lp, questions + users + 1);
  for (col = 1; col <= questions; ++col)
  {
    glp_set_obj_coef(lp, col, 0.0);
    glp_set_col_kind(lp, col, GLP_BV);
  }
  for (col = 1; col <= users; ++col)
  {
    glp_set_obj_coef(lp, questions + col, 0.0);
    glp_set_col_kind(lp, questions + col, GLP_IV);
    glp_set_col_bnds(lp, questions + col, GLP_FR, 0.0, 0.0);
  }
  glp_set_obj_coef(lp, questions+users+1, 1.0);
  glp_set_col_kind(lp, questions+users+1, GLP_IV);
  glp_set_col_bnds(lp, questions+users+1, GLP_FR, 0.0, 0.0);

  // Configure matrix (question columns)
  for(col = 1; col <= questions; ++col)
  {
    for (row = 1; row <= users*2; ++row)
    {
      ind[row] = row;
      val[row] = ((row <= users) && (rand() % 2))? 1.0: 0.0;
    }
    ind[users*2 + 1] = users*2 + 1;
    val[users*2 + 1] = 1.0;
    glp_set_mat_col(lp, col, users*2 + 1, ind, val);
  }

  // Configure matrix (user columns)
  for(col = 1; col <= users; ++col)
  {
    for (row = 1; row <= users*2; ++row)
    {
      ind[row] = row;
      val[row] = (row == col)? -1.0: ((row == col + users)? 1.0: 0.0);
    }
    ind[users*2 + 1] = users*2 + 1;
    val[users*2 + 1] = 0.0;
    glp_set_mat_col(lp, questions + col, users*2 + 1, ind, val);
  }

  // Configure matrix (score column)
  for (row = 1; row <= users*2; ++row)
  {
    ind[row] = row;
    val[row] = (row > users)? -1.0: 0.0;
  }
  ind[users*2 + 1] = users*2 + 1;
  val[users*2 + 1] = 0.0;
  glp_set_mat_col(lp, questions + users + 1, users*2 + 1, ind, val);

  // Solve integer GLPK problem
  glp_iocp param;
  glp_init_iocp(&param);
  param.presolve = GLP_ON;
  param.tm_lim = time * 1000;
  glp_intopt(lp, &param);
  printf("Score = %g\n", glp_mip_obj_val(lp));

  glp_delete_prob(lp);
  return 0;
}

В моих тестах ограничение по времени не работает надежно. Похоже, какая-то ошибка в GLPK ...

Пример кода для варианта 2 (только алгоритм LP, без автоматического поиска Score):

#include <stdio.h>
#include <stdlib.h>
#include <glpk.h>

int main(void)
{
  int ind[3000];
  double val[3000];
  int row;
  int col;
  glp_prob *lp;

  // Parameters
  int users = 120;
  int questions = 10000;
  int questions2 = questions - 30;
  double score = 4869.0 + 7;

  // Create GLPK problem
  lp = glp_create_prob();
  glp_set_prob_name(lp, "questions");
  glp_set_obj_dir(lp, GLP_MAX);

  // Configure rows
  glp_add_rows(lp, users + 1);
  for (row = 1; row <= users; ++row)
  {
    glp_set_row_bnds(lp, row, GLP_LO, score, score);
  }
  glp_set_row_bnds(lp, users + 1, GLP_FX, questions2, questions2);

  // Configure columns
  glp_add_cols(lp, questions);
  for (col = 1; col <= questions; ++col)
  {
    glp_set_obj_coef(lp, col, 0.0);
    glp_set_col_kind(lp, col, GLP_BV);
  }

  // Configure matrix (question columns)
  for(col = 1; col <= questions; ++col)
  {
    for (row = 1; row <= users; ++row)
    {
      ind[row] = row;
      val[row] = (rand() % 2)? 1.0: 0.0;
    }
    ind[users + 1] = users + 1;
    val[users + 1] = 1.0;
    glp_set_mat_col(lp, col, users + 1, ind, val);
  }

  // Solve integer GLPK problem
  glp_iocp param;
  glp_init_iocp(&param);
  param.presolve = GLP_ON;
  glp_intopt(lp, &param);

  glp_delete_prob(lp);
  return 0;
}

Похоже, что вариант 2 позволяет довольно быстро найти хорошее приближение. И приближение лучше, чем для варианта 1.

1 голос
/ 11 января 2012

Вот целочисленная программа.Пусть константа unseen(i, j) будет 1, если игрок i не видел вопросов j и 0 в противном случае.Пусть переменная kept(j) будет 1, если вопрос j будет сохранен, и 0 в противном случае.Пусть переменная score будет целью.

maximize score                                       # score is your objective
subject to

for all i,  score <= sum_j (unseen(i, j) * kept(j))  # score is at most
                                                     # the number of questions
                                                     # available to player i

sum_j (1 - kept(j)) = 30                             # remove exactly
                                                     # 30 questions

for all j,  kept(j) in {0, 1}                        # each question is kept
                                                     # or not kept (binary)

(score has no preset bound; the optimal solution chooses score
 to be the minimum over all players of the number of questions
 available to that player)
1 голос
/ 11 января 2012

Представление этого с точки зрения вопросов по-прежнему играбельно.Я буду нумеровать вопросы от 0 до 4 вместо 1 до 5, так как это более удобно при программировании.

          01234
          -----
player A   x x   - player A has just 2 playable questions
player B   xx x  - player B has 3 playable questions
player C  x x x  - player C has 3 playable questions

Сначала я опишу то, что может показаться очень наивным алгоритмом, но вВ конце я покажу, как его можно значительно улучшить.

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

vector<bool> keep_or_discard(5); // an array to store the five decisions

void decide_one_question(int question_id) {
    // first, pretend we keep the question
    keep_or_discard[question_id] = true;
    decide_one_question(question_id + 1); // recursively consider the next question

    // then, pretend we discard this question
    keep_or_discard[question_id] = false;
    decide_one_question(question_id + 1); // recursively consider the next question
}

decide_one_question(0); // this call starts the whole recursive search

. Эта первая попытка приведет к бесконечному рекурсивному спуску и пройдет за конец массива.Очевидное первое, что нам нужно сделать, это немедленно вернуться, когда question_id == 5 (то есть, когда все вопросы с 0 по 4 были решены. Мы добавляем этот код в начало решить____для:

void decide_one_question(int question_id) {
    {
        if(question_id == 5) {
            // no more decisions needed.
            return;
        }
    }
    // ....

Далее,мы знаем, сколько вопросов нам разрешено хранить. Назовите это allowed_to_keep. В данном случае это 5-3, что означает, что мы должны оставить ровно два вопроса. Вы можете установить это как глобальную переменную где-нибудь.

int allowed_to_keep; // set this to 2

Теперь мы должны добавить дополнительные проверки в начало define_one_question и добавить еще один параметр:

void decide_one_question(int question_id, int questions_kept_so_far) {
    {
        if(question_id == 5) {
            // no more decisions needed.
            return;
        }
        if(questions_kept_so_far > allowed_to_keep) {
            // not allowed to keep this many, just return immediately
            return;
        }
        int questions_left_to_consider = 5 - question_id; // how many not yet considered
        if(questions_kept_so_far + questions_left_to_consider < allowed_to_keep) {
            // even if we keep all the rest, we'll fall short
            // may as well return. (This is an optional extra)
            return;
        }
    }

    keep_or_discard[question_id] = true;
    decide_one_question(question_id + 1, questions_kept_so_far + 1);

    keep_or_discard[question_id] = false;
    decide_one_question(question_id + 1, questions_kept_so_far );
}

decide_one_question(0,0);

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

Пока что это выглядит довольно наивно.Это проверка каждой комбинации. Терпите меня!

Мы должны начать отслеживать счет, чтобы запомнить лучшее (и впарация для дальнейшей оптимизации).Первым делом нужно написать функцию calculate_score.И иметь глобальный под названием best_score_so_far.Наша цель - максимизировать его, поэтому в начале алгоритма это должно быть инициализировано до -1.

int best_score_so_far; // initialize to -1 at the start

void decide_one_question(int question_id, int questions_kept_so_far) {
    {
        if(question_id == 5) {
            int score = calculate_score();
            if(score > best_score_so_far) {
                // Great!
                best_score_so_far = score;
                store_this_good_set_of_answers();
            }
            return;
        }
        // ...

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

void decide_one_question(int question_id
                       , int questions_kept_so_far
                       , int upper_bound_on_the_score) {
     ... the checks we've already detailed above

    keep_or_discard[question_id] = true;
    decide_one_question(question_id + 1
          , questions_kept_so_far + 1
          , upper_bound_on_the_score
        );

    keep_or_discard[question_id] = false;

    decide_one_question(question_id + 1
          , questions_kept_so_far
          , calculate_the_new_upper_bound()
        );

См.ближе к концу этого последнего фрагмента кода была вычислена новая (меньшая) верхняя граница на основе решения об отклонении вопроса 'question_id'.

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

Оптимизация

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

void decide_one_question(int question_id
                       , int questions_kept_so_far
                       , upper_bound_on_the_score) {
        if(upper_bound_on_the_score < best_score_so_far) {
            // the upper bound is already too low,
            // therefore, this is a dead end.
            return;
        }
        if(question_id == 5) // .. continue with the rest of the function.

Эта проверка гарантирует, что как только будет найдено «разумное» решение, алгоритм быстро откажется от всех «тупиковых» поисков.Затем (надеюсь) он быстро найдет лучшие и лучшие решения, а затем может быть еще более агрессивным в деле обрезки мертвых веток.Я обнаружил, что этот подход довольно хорошо работает на практике.

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

1 голос
/ 05 января 2012

Рассматривали ли вы просмотр этого с точки зрения решения для динамического программирования?

Я думаю, что вы могли бы сделать это, максимально увеличив количество доступных вопросов, оставшихся открытыми. всем игрокам, так что не осталось ни одного игрока с нулевыми открытыми вопросами.

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

1 голос
/ 05 января 2012

Допустим, вы хотите удалить Y вопросов из пула. Простой алгоритм будет сортировать вопросы по количеству просмотров. Затем вы удаляете Y самых просматриваемых вопросов. Для вашего примера: 1: 2, 2: 1, 3: 1, 4: 2, 5: 1. Очевидно, что вам лучше удалить вопросы 1 и 4. Но этот алгоритм не достигает цели. Тем не менее, это хорошая отправная точка. Чтобы улучшить его, вам нужно убедиться, что после «очистки» у каждого пользователя будет как минимум X вопросов.

В дополнение к вышеуказанному массиву (который мы можем назвать «счетом»), вам нужен второй с вопросами и пользователями, где пересечение будет иметь 1, если пользователь видел вопрос, и 0, если он не видел. Затем для каждого пользователя вам нужно найти X вопросов с наименьшим баллом править: что он еще не видел (чем меньше их баллов, тем лучше, поскольку чем меньше людей увидят вопрос, тем больше " ценный "это для системы в целом). Вы объединяете все найденные X вопросы от каждого пользователя в третий массив, давайте назовем его «безопасным», так как мы не будем удалять из него ни одного.

В качестве последнего шага вы просто удаляете Y наиболее просматриваемые вопросы (те, у которых самый высокий балл), которых нет в «безопасном» массиве.

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

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

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

0 голосов
/ 11 января 2012

вопрос сначала кажется легким, но после более глубокого осмысления вы понимаете, насколько он сложен.

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

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

Я думаю, было бы разумно ограничить количество оставшихся вопросов для пользователя разумным значением.Вы можете подумать: «Хорошо, у этого пользователя достаточно вопросов для просмотра, если у него больше X вопросов».Вам это нужно, потому что после удаления вопроса для активного пользователя может остаться только 15 вопросов, а для редкого посетителя может остаться 500 вопросов.Неверно суммировать 15 и 500. Вместо этого вы можете определить пороговое значение 100.

Чтобы упростить вычисления, вы можете рассмотреть только пользователей, которые просмотрели более X вопросов.

0 голосов
/ 11 января 2012

Если существует слишком много вариантов грубой силы, и, вероятно, существует много решений, которые почти оптимальны (звучит так), рассмотрите методы Монте-Карло.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...