Краща оцінка баггальської дошки


16

Мені було цікаво побачити відповіді на це (зараз неіснуюче) питання , але воно ніколи не було виправлене / покращене.

Враховуючи набір 6-сторонніх кубиків Boggle (конфігурація, викрадена з цього питання ), визначте за дві хвилини часу обробки, яка конфігурація плати дозволить отримати максимально високий бал. .


МЕТА

  • Ваш код повинен працювати не більше 2 хвилин (120 секунд). У цей час він повинен автоматично припинити роботу та друкувати результати.

  • Остаточним балом виклику буде середній багль від 5 циклів програми.

    • У випадку зрівноваження переможцем буде той алгоритм, який знайде більше слів.
    • У випадку, якщо все-таки залишилося зрівноважене, переможцем буде той алгоритм, який знайде більше довгих (8+) слів.

ПРАВИЛА / ОГРАНИЧЕННЯ

  • Це виклик коду; довжина коду не має значення.

  • Перейдіть за цим посиланням для списку слів (використовуйте ISPELL "english.0"список - у списку SCOWL відсутні деякі досить поширені слова).

    • Цей перелік може бути посилатися / імпортуватися / читати у вашому коді будь-яким способом.
    • ^([a-pr-z]|qu){3,16}$Будуть зараховані лише слова, відповідні з регулярним виразом . (В якості одиниці повинні використовуватися лише малі літери, 3-16 символів, qu.)
  • Слова утворюються шляхом зв’язування сусідніх літер (горизонтальної, вертикальної та діагональної) для написання слів у правильному порядку, не вживаючи жодної крапки більше одного разу в одному слові.

    • Слова повинні бути 3 літери або довше; коротші слова не зароблять балів.
    • Дублікати букв є прийнятними, тільки не кістки.
    • Слова, які охоплюють краї / перехрещуються з одного боку дошки на інший, забороняються.
  • Остаточний багл ( не виклик ) - це загальна кількість балів за всі слова, які знайдені.

    • Значення балів, присвоєне кожному слову, ґрунтується на довжині слова. (Дивіться нижче)
    • Звичайні правила Googlegle віднімають / знижують слова, знайдені іншим гравцем. Припустимо, тут не залучаються інші гравці, і всі знайдені слова зараховуються до загальної кількості балів.
    • Однак слова, знайдені не раз в одній сітці, слід рахувати лише один раз.
  • Ваша функція / програма повинні ЗНАЙТИ оптимальне розташування; просто жорстке кодування заздалегідь визначеного списку не зробить.

  • Вихід повинен бути 4х4 сіткою вашої ідеальної ігрової дошки, переліком усіх знайдених слів для цієї дошки та багл-балом, щоб відповідати цим словам.


СМЕРТИ КОНФІГУРАЦІЮ

A  A  E  E  G  N
E  L  R  T  T  Y
A  O  O  T  T  W
A  B  B  J  O  O
E  H  R  T  V  W
C  I  M  O  T  U
D  I  S  T  T  Y
E  I  O  S  S  T
D  E  L  R  V  Y
A  C  H  O  P  S
H  I  M  N  Qu U
E  E  I  N  S  U
E  E  G  H  N  W
A  F  F  K  P  S
H  L  N  N  R  Z
D  E  I  L  R  X

СТАНДАРТНИЙ БОГЛЕ СКОРОЗНИЙ СТОЛ

Word length => Points
<= 2 - 0 pts
   3 - 1  
   4 - 1  
   5 - 2  
   6 - 3  
   7 - 5
>= 8 - 11 pts
*Words using the "Qu" die will count the full 2 letters for their word, not just the 1 die.

ПРИКЛАД ВИХІД

A  L  O  J  
V  U  T  S  
L  C  H  E  
G  K  R  X

CUT
THE
LUCK
HEX
....

140 points

Якщо потрібно додаткове роз'яснення, будь ласка, запитайте!


2
Я хотів би мати словник для стандартизації мети. Зауважте також, що це не нова ідея, оскільки простий пошук у Google виявить :) Найвищий бал, який я бачив, 4527( 1414кількість слів), знайдений тут: ai.stanford.edu/~chuongdo/boggle/index.html
mellamokb

4
Чи потрібно програму припинити це століття?
Пітер Тейлор

1
@GlitchMr Англійською мовою Q, як правило, коли-небудь використовується лише для облікових записів U. Boggle, ставлячи дві літери на одну матрицю, як одну одиницю.
Гаффі

1
Специфікація списку слів незрозуміла. Чи вважаєте ви лише ті слова, які перелічені англійською мовою.0 в нижньому регістрі? (Стандартні правила гри в слова виключають скорочення / ініціалізми та власні іменники).
Пітер Тейлор

1
Я думав про регулярний вираз ^([a-pr-z]|qu){3,16}$(який би неправильно виключав 3-літерні слова з qu, але таких немає).
Пітер Тейлор

Відповіді:


9

C, в середньому 500+ 1500 1750 балів

Це порівняно незначне покращення порівняно з версією 2 (див. Нижче для приміток до попередніх версій). Є дві частини. По-перше: Замість вибору дошки випадковим чином із пулу програма тепер перебирає кожну дошку в пулі, використовуючи кожну по черзі перед поверненням до вершини пулу та повторенням. (Оскільки пул модифікується під час цієї ітерації, все одно залишаться дошки, які обираються двічі поспіль, або ще гірше, але це не викликає серйозних проблем.) Друга зміна полягає в тому, що програма відстежує, коли пул змінюється , і якщо програма триває занадто довго, не покращуючи вміст пулу, він визначає, що пошук "застопорився", спорожняє пул і починається з нового пошуку. Це триває до тих пір, поки не вичерпаються дві хвилини.

Спочатку я думав, що буду використовувати якийсь евристичний пошук, щоб вийти за рамки 1500 балів. @ коментар mellamokb щодо плати на 4527 балів змусив мене припустити, що є багато можливостей для вдосконалення. Однак ми використовуємо порівняно невеликий список слів. Дошка з 4527 балами підрахувала за допомогою YAWL, який є найбільш включеним списком слів там - він навіть більший, ніж офіційний список слів США Scrabble. Маючи це на увазі, я переглянув ради, знайдені моєю програмою, і помітив, що існує певний набір дощок понад 1700. Так, наприклад, у мене було декілька пробіжок, які виявили дошку, набравши 1726, але завжди була виявлена ​​та сама та сама дошка (ігнорування обертань та роздумів).

Як черговий тест, я запустив свою програму, використовуючи YAWL як словник, і вона знайшла 4527-бальну плату приблизно через десяток пробіжок. Враховуючи це, я гіпотезую, що моя програма вже знаходиться на верхній межі простору пошуку, і тому перезапис, який я планував, введе додаткову складність для дуже невеликого виграшу.

Ось мій список п’яти дошки з найвищими балами, яку знайшла моя програма за допомогою english.0списку слів:

1735 :  D C L P  E I A E  R N T R  S E G S
1738 :  B E L S  R A D G  T I N E  S E R S
1747 :  D C L P  E I A E  N T R D  G S E R
1766 :  M P L S  S A I E  N T R N  D E S G
1772:   G R E P  T N A L  E S I T  D R E S

Я вважаю, що "греп-борд" 1772 року (як я прийняв його називати), з 531 словами, є найвищою оцінною дошкою з цього списку слів. Більше 50% двохвилинних пробіжок моєї програми закінчується цією дошкою. Я також залишив свою програму протягом ночі, не знайшовши нічого кращого. Отже, якщо є дошка з більш високим балом, вона, ймовірно, повинна мати якийсь аспект, який перемагає техніку пошуку програми. Дошка, на якій кожна можлива невелика зміна макета спричиняє величезне падіння загальної оцінки, наприклад, ніколи не може бути виявлена ​​моєю програмою. Моя думка полягає в тому, що така рада дуже навряд чи існуватиме.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#define WORDLISTFILE "./english.0"

#define XSIZE 4
#define YSIZE 4
#define BOARDSIZE (XSIZE * YSIZE)
#define DIEFACES 6
#define WORDBUFSIZE 256
#define MAXPOOLSIZE 32
#define STALLPOINT 64
#define RUNTIME 120

/* Generate a random int from 0 to N-1.
 */
#define random(N)  ((int)(((double)(N) * rand()) / (RAND_MAX + 1.0)))

static char const dice[BOARDSIZE][DIEFACES] = {
    "aaeegn", "elrtty", "aoottw", "abbjoo",
    "ehrtvw", "cimotu", "distty", "eiosst",
    "delrvy", "achops", "himnqu", "eeinsu",
    "eeghnw", "affkps", "hlnnrz", "deilrx"
};

/* The dictionary is represented in memory as a tree. The tree is
 * represented by its arcs; the nodes are implicit. All of the arcs
 * emanating from a single node are stored as a linked list in
 * alphabetical order.
 */
typedef struct {
    int letter:8;   /* the letter this arc is labelled with */
    int arc:24;     /* the node this arc points to (i.e. its first arc) */
    int next:24;    /* the next sibling arc emanating from this node */
    int final:1;    /* true if this arc is the end of a valid word */
} treearc;

/* Each of the slots that make up the playing board is represented
 * by the die it contains.
 */
typedef struct {
    unsigned char die;      /* which die is in this slot */
    unsigned char face;     /* which face of the die is showing */
} slot;

/* The following information defines a game.
 */
typedef struct {
    slot board[BOARDSIZE];  /* the contents of the board */
    int score;              /* how many points the board is worth */
} game;

/* The wordlist is stored as a binary search tree.
 */
typedef struct {
    int item: 24;   /* the identifier of a word in the list */
    int left: 16;   /* the branch with smaller identifiers */
    int right: 16;  /* the branch with larger identifiers */
} listnode;

/* The dictionary.
 */
static treearc *dictionary;
static int heapalloc;
static int heapsize;

/* Every slot's immediate neighbors.
 */
static int neighbors[BOARDSIZE][9];

/* The wordlist, used while scoring a board.
 */
static listnode *wordlist;
static int listalloc;
static int listsize;
static int xcursor;

/* The game that is currently being examined.
 */
static game G;

/* The highest-scoring game seen so far.
 */
static game bestgame;

/* Variables to time the program and display stats.
 */
static time_t start;
static int boardcount;
static int allscores;

/* The pool contains the N highest-scoring games seen so far.
 */
static game pool[MAXPOOLSIZE];
static int poolsize;
static int cutoffscore;
static int stallcounter;

/* Some buffers shared by recursive functions.
 */
static char wordbuf[WORDBUFSIZE];
static char gridbuf[BOARDSIZE];

/*
 * The dictionary is stored as a tree. It is created during
 * initialization and remains unmodified afterwards. When moving
 * through the tree, the program tracks the arc that points to the
 * current node. (The first arc in the heap is a dummy that points to
 * the root node, which otherwise would have no arc.)
 */

static void initdictionary(void)
{
    heapalloc = 256;
    dictionary = malloc(256 * sizeof *dictionary);
    heapsize = 1;
    dictionary->arc = 0;
    dictionary->letter = 0;
    dictionary->next = 0;
    dictionary->final = 0;
}

static int addarc(int arc, char ch)
{
    int prev, a;

    prev = arc;
    a = dictionary[arc].arc;
    for (;;) {
        if (dictionary[a].letter == ch)
            return a;
        if (!dictionary[a].letter || dictionary[a].letter > ch)
            break;
        prev = a;
        a = dictionary[a].next;
    }
    if (heapsize >= heapalloc) {
        heapalloc *= 2;
        dictionary = realloc(dictionary, heapalloc * sizeof *dictionary);
    }
    a = heapsize++;
    dictionary[a].letter = ch;
    dictionary[a].final = 0;
    dictionary[a].arc = 0;
    if (prev == arc) {
        dictionary[a].next = dictionary[prev].arc;
        dictionary[prev].arc = a;
    } else {
        dictionary[a].next = dictionary[prev].next;
        dictionary[prev].next = a;
    }
    return a;
}

static int validateword(char *word)
{
    int i;

    for (i = 0 ; word[i] != '\0' && word[i] != '\n' ; ++i)
        if (word[i] < 'a' || word[i] > 'z')
            return 0;
    if (word[i] == '\n')
        word[i] = '\0';
    if (i < 3)
        return 0;
    for ( ; *word ; ++word, --i) {
        if (*word == 'q') {
            if (word[1] != 'u')
                return 0;
            memmove(word + 1, word + 2, --i);
        }
    }
    return 1;
}

static void createdictionary(char const *filename)
{
    FILE *fp;
    int arc, i;

    initdictionary();
    fp = fopen(filename, "r");
    while (fgets(wordbuf, sizeof wordbuf, fp)) {
        if (!validateword(wordbuf))
            continue;
        arc = 0;
        for (i = 0 ; wordbuf[i] ; ++i)
            arc = addarc(arc, wordbuf[i]);
        dictionary[arc].final = 1;
    }
    fclose(fp);
}

/*
 * The wordlist is stored as a binary search tree. It is only added
 * to, searched, and erased. Instead of storing the actual word, it
 * only retains the word's final arc in the dictionary. Thus, the
 * dictionary needs to be walked in order to print out the wordlist.
 */

static void initwordlist(void)
{
    listalloc = 16;
    wordlist = malloc(listalloc * sizeof *wordlist);
    listsize = 0;
}

static int iswordinlist(int word)
{
    int node, n;

    n = 0;
    for (;;) {
        node = n;
        if (wordlist[node].item == word)
            return 1;
        if (wordlist[node].item > word)
            n = wordlist[node].left;
        else
            n = wordlist[node].right;
        if (!n)
            return 0;
    }
}

static int insertword(int word)
{
    int node, n;

    if (!listsize) {
        wordlist->item = word;
        wordlist->left = 0;
        wordlist->right = 0;
        ++listsize;
        return 1;
    }

    n = 0;
    for (;;) {
        node = n;
        if (wordlist[node].item == word)
            return 0;
        if (wordlist[node].item > word)
            n = wordlist[node].left;
        else
            n = wordlist[node].right;
        if (!n)
            break;
    }

    if (listsize >= listalloc) {
        listalloc *= 2;
        wordlist = realloc(wordlist, listalloc * sizeof *wordlist);
    }
    n = listsize++;
    wordlist[n].item = word;
    wordlist[n].left = 0;
    wordlist[n].right = 0;
    if (wordlist[node].item > word)
        wordlist[node].left = n;
    else
        wordlist[node].right = n;
    return 1;
}

static void clearwordlist(void)
{
    listsize = 0;
    G.score = 0;
}


static void scoreword(char const *word)
{
    int const scoring[] = { 0, 0, 0, 1, 1, 2, 3, 5 };
    int n, u;

    for (n = u = 0 ; word[n] ; ++n)
        if (word[n] == 'q')
            ++u;
    n += u;
    G.score += n > 7 ? 11 : scoring[n];
}

static void addwordtolist(char const *word, int id)
{
    if (insertword(id))
        scoreword(word);
}

static void _printwords(int arc, int len)
{
    int a;

    while (arc) {
        a = len + 1;
        wordbuf[len] = dictionary[arc].letter;
        if (wordbuf[len] == 'q')
            wordbuf[a++] = 'u';
        if (dictionary[arc].final) {
            if (iswordinlist(arc)) {
                wordbuf[a] = '\0';
                if (xcursor == 4) {
                    printf("%s\n", wordbuf);
                    xcursor = 0;
                } else {
                    printf("%-16s", wordbuf);
                    ++xcursor;
                }
            }
        }
        _printwords(dictionary[arc].arc, a);
        arc = dictionary[arc].next;
    }
}

static void printwordlist(void)
{
    xcursor = 0;
    _printwords(1, 0);
    if (xcursor)
        putchar('\n');
}

/*
 * The board is stored as an array of oriented dice. To score a game,
 * the program looks at each slot on the board in turn, and tries to
 * find a path along the dictionary tree that matches the letters on
 * adjacent dice.
 */

static void initneighbors(void)
{
    int i, j, n;

    for (i = 0 ; i < BOARDSIZE ; ++i) {
        n = 0;
        for (j = 0 ; j < BOARDSIZE ; ++j)
            if (i != j && abs(i / XSIZE - j / XSIZE) <= 1
                       && abs(i % XSIZE - j % XSIZE) <= 1)
                neighbors[i][n++] = j;
        neighbors[i][n] = -1;
    }
}

static void printboard(void)
{
    int i;

    for (i = 0 ; i < BOARDSIZE ; ++i) {
        printf(" %c", toupper(dice[G.board[i].die][G.board[i].face]));
        if (i % XSIZE == XSIZE - 1)
            putchar('\n');
    }
}

static void _findwords(int pos, int arc, int len)
{
    int ch, i, p;

    for (;;) {
        ch = dictionary[arc].letter;
        if (ch == gridbuf[pos])
            break;
        if (ch > gridbuf[pos] || !dictionary[arc].next)
            return;
        arc = dictionary[arc].next;
    }
    wordbuf[len++] = ch;
    if (dictionary[arc].final) {
        wordbuf[len] = '\0';
        addwordtolist(wordbuf, arc);
    }
    gridbuf[pos] = '.';
    for (i = 0 ; (p = neighbors[pos][i]) >= 0 ; ++i)
        if (gridbuf[p] != '.')
            _findwords(p, dictionary[arc].arc, len);
    gridbuf[pos] = ch;
}

static void findwordsingrid(void)
{
    int i;

    clearwordlist();
    for (i = 0 ; i < BOARDSIZE ; ++i)
        gridbuf[i] = dice[G.board[i].die][G.board[i].face];
    for (i = 0 ; i < BOARDSIZE ; ++i)
        _findwords(i, 1, 0);
}

static void shuffleboard(void)
{
    int die[BOARDSIZE];
    int i, n;

    for (i = 0 ; i < BOARDSIZE ; ++i)
        die[i] = i;
    for (i = BOARDSIZE ; i-- ; ) {
        n = random(i);
        G.board[i].die = die[n];
        G.board[i].face = random(DIEFACES);
        die[n] = die[i];
    }
}

/*
 * The pool contains the N highest-scoring games found so far. (This
 * would typically be done using a priority queue, but it represents
 * far too little of the runtime. Brute force is just as good and
 * simpler.) Note that the pool will only ever contain one board with
 * a particular score: This is a cheap way to discourage the pool from
 * filling up with almost-identical high-scoring boards.
 */

static void addgametopool(void)
{
    int i;

    if (G.score < cutoffscore)
        return;
    for (i = 0 ; i < poolsize ; ++i) {
        if (G.score == pool[i].score) {
            pool[i] = G;
            return;
        }
        if (G.score > pool[i].score)
            break;
    }
    if (poolsize < MAXPOOLSIZE)
        ++poolsize;
    if (i < poolsize) {
        memmove(pool + i + 1, pool + i, (poolsize - i - 1) * sizeof *pool);
        pool[i] = G;
    }
    cutoffscore = pool[poolsize - 1].score;
    stallcounter = 0;
}

static void selectpoolmember(int n)
{
    G = pool[n];
}

static void emptypool(void)
{
    poolsize = 0;
    cutoffscore = 0;
    stallcounter = 0;
}

/*
 * The program examines as many boards as it can in the given time,
 * and retains the one with the highest score. If the program is out
 * of time, then it reports the best-seen game and immediately exits.
 */

static void report(void)
{
    findwordsingrid();
    printboard();
    printwordlist();
    printf("score = %d\n", G.score);
    fprintf(stderr, "// score: %d points (%d words)\n", G.score, listsize);
    fprintf(stderr, "// %d boards examined\n", boardcount);
    fprintf(stderr, "// avg score: %.1f\n", (double)allscores / boardcount);
    fprintf(stderr, "// runtime: %ld s\n", time(0) - start);
}

static void scoreboard(void)
{
    findwordsingrid();
    ++boardcount;
    allscores += G.score;
    addgametopool();
    if (bestgame.score < G.score) {
        bestgame = G;
        fprintf(stderr, "// %ld s: board %d scoring %d\n",
                time(0) - start, boardcount, G.score);
    }

    if (time(0) - start >= RUNTIME) {
        G = bestgame;
        report();
        exit(0);
    }
}

static void restartpool(void)
{
    emptypool();
    while (poolsize < MAXPOOLSIZE) {
        shuffleboard();
        scoreboard();
    }
}

/*
 * Making small modifications to a board.
 */

static void turndie(void)
{
    int i, j;

    i = random(BOARDSIZE);
    j = random(DIEFACES - 1) + 1;
    G.board[i].face = (G.board[i].face + j) % DIEFACES;
}

static void swapdice(void)
{
    slot t;
    int p, q;

    p = random(BOARDSIZE);
    q = random(BOARDSIZE - 1);
    if (q >= p)
        ++q;
    t = G.board[p];
    G.board[p] = G.board[q];
    G.board[q] = t;
}

/*
 *
 */

int main(void)
{
    int i;

    start = time(0);
    srand((unsigned int)start);

    createdictionary(WORDLISTFILE);
    initwordlist();
    initneighbors();

    restartpool();
    for (;;) {
        for (i = 0 ; i < poolsize ; ++i) {
            selectpoolmember(i);
            turndie();
            scoreboard();
            selectpoolmember(i);
            swapdice();
            scoreboard();
        }
        ++stallcounter;
        if (stallcounter >= STALLPOINT) {
            fprintf(stderr, "// stalled; restarting search\n");
            restartpool();
        }
    }

    return 0;
}

Примітки до версії 2 (9 червня)

Ось один із способів використовувати початкову версію мого коду як точку відключення. Зміни цієї версії складаються з менш ніж 100 рядків, але втричі перевищують середній бал гри.

У цій версії програма підтримує "пул" кандидатів, що складається з N найвищих дощових балів, які програма створила до цього часу. Кожен раз, коли генерується нова дошка, вона додається до пулу і видаляється дошка з найнижчою кількістю балів у пулі (що може бути цілком доданою дошкою, якщо її оцінка нижча за ту, яка вже є). Пул спочатку заповнюється довільно генерованими дошками, після чого він зберігає постійний розмір протягом усього виконання програми.

Основний цикл програми складається з вибору випадкової дошки з пулу та її зміни, визначення цієї нової дошки та додавання її в пул (якщо вона набирає достатньо успішного результату). Таким чином програма постійно вдосконалює дошки з високим балом. Основна діяльність полягає в поетапних, поступових удосконаленнях, але розмір пулу також дозволяє програмі знаходити багатоступінчасті вдосконалення, які тимчасово погіршують результат роботи дошки, перш ніж вона зможе покращити її.

Зазвичай ця програма досить швидко знаходить хороший локальний максимум, після чого, мабуть, будь-який кращий максимум занадто віддалений, щоб його знайти. І тому ще раз мало сенсу запускати програму довше 10 секунд. Це може бути покращено, наприклад, якщо програма виявить цю ситуацію та розпочнеть новий пошук з новим набором кандидатів. Однак це призведе до лише незначного збільшення. Правильна евристична техніка пошуку, ймовірно, стане кращим способом розвідки.

(Побічна примітка: я бачив, що ця версія генерує близько 5 тис. Дощок / сек. Оскільки перша версія зазвичай робила дошки 20 к / с, я спочатку була стурбована. Профілюючи, однак, я виявила, що додатковий час витрачається на управління списком слів. Іншими словами, це було повністю пов’язано з тим, що програма знайшла так багато слів на дошці. У світлі цього я розглядав можливість зміни коду для управління списком слів, але враховуючи, що ця програма використовує лише 10 своїх відведених 120 секунд, наприклад оптимізація була б дуже передчасною.)

Примітки до версії 1 (2 червня)

Це дуже-дуже просте рішення. Все, що він робить, генерує випадкові дошки, а потім через 10 секунд видає ту, яка має найвищий бал. (Я встановив дефолт до 10 секунд, оскільки додаткові 110 секунд, дозволені специфікацією проблеми, як правило, не покращують кінцеве рішення, знайдене достатньо, щоб його варто було чекати.) Отже, це надзвичайно глупо. Однак у нього є вся інфраструктура, щоб зробити хороший вихідний пункт для більш розумного пошуку, і якщо хтось хоче скористатись цим до крайнього терміну, я закликаю їх зробити це.

Програма починається з читання словника в структурі дерева. (Форма не настільки оптимізована, як це могла б бути, але для цих цілей вона є більш ніж хорошою.) Після іншої основної ініціалізації вона починає генерувати дошки та оцінювати їх. Програма вивчає близько 20 тис. Дощок в секунду на моїй машині, і приблизно через 200 тис. Дощок випадковий підхід починає сушитись.

Оскільки в будь-який момент фактично оцінюється лише одна дошка, дані балів зберігаються у глобальних змінних. Це дозволяє мені мінімізувати кількість постійних даних, які мають бути передані як аргументи рекурсивним функціям. (Я впевнений, що це дасть людям вулики, і я їм вибачте.) Список слів зберігається як двійкове дерево пошуку. Кожне знайдене слово потрібно шукати у списку слів, щоб повторювані слова не рахувалися двічі. Список слів потрібен лише під час процесу еваляції, тому він відкидається після того, як оцінка буде знайдена. Таким чином, наприкінці програми обрану дошку потрібно заздалегідь оцінювати, щоб список друку слів можна було роздрукувати.

Факт забави: Середній бал для довільно сформованої дошки Boggle, набраний ними english.0, становить 61,7 балів.


Очевидно, що мені потрібно підвищити власну ефективність. :-)
Гаффі

Мій генетичний підхід отримує близько 700-800 балів, генеруючи близько 200 тис. Дощок, тож ви чітко робите щось набагато краще, ніж я, у тому, як ви виробляєте наступне покоління.
Пітер Тейлор

Моя власна структура дерева, яка до цього часу була реалізована лише для головного списку слів, хоча вона працює і дозволяє мені перевіряти дошки, вона знижує мою системну пам'ять (активно відстає до того, що потрібно багато часу, щоб примусити процес припинити достроково). Це, безумовно, моя вина, але я працюю над цим! Редагувати: Виправлено! ;-)
Гаффі

@PeterTaylor Я думав про спробу генетичного алгоритму, але не міг придумати правдоподібний механізм поєднання двох плат. Як ти це робиш? Чи вибираєте ви батька випадковим чином для кожного слота на дошці?
хлібопічка

Я розділив стан дошки на перестановку кубиків і обличчя, що показуються на кістях. Для перестановки перестановки я використовую "замовити кросовер 1" від cs.colostate.edu/~genitor/1995/permutations.pdf, а для особового кросовера я роблю очевидне. Але в мене є ідея абсолютно іншого підходу, який мені потрібно знайти час для реалізації.
Пітер Тейлор

3

VBA (середній зараз 80-110 балів, незакінчений)

Ось мій робочий процес, але це далеко не найкращий можливий; мій абсолютний найкращий бал, знайдений на будь-якій дошці після багатьох пробних пробіжок, становить близько 120. Ще потрібно покращити загальну очистку, і я впевнений, що в багатьох місцях потрібно досягти більшої ефективності.

  • 2012.05.09:
    • Оригінальна публікація
  • 2012.05.10 - 18.05.2012:
    • Удосконалено алгоритм оцінювання
    • Удосконалена логіка простеження
  • 2012.06.07 - 2012.06.12 :
    • Зменшено обмеження слів до 6 з 8. Дозволяє отримати більше дощок із меншими словами. Схоже, незначне поліпшення середнього балу. (10-15 дощок, що перевіряються за пробіг, проти 1 до 2)
    • Після пропозиції хлібопечки я створив структуру дерева для розміщення списку слів. Це значно прискорить зворотну перевірку слів на дошці.
    • Я грав зі зміною максимального розміру слів (швидкість та оцінка), і я ще не вирішив, чи краще 5 або 6 є кращим варіантом для мене. 6 результатів було перевірено в 100-120 загальних дощок, а 5 - у 500-1000 (обидва з них ще далеко за іншими прикладами, поданими до цього часу).
    • Проблема : Після багатьох послідовних запусків процес починає сповільнюватися, тому є ще певна пам'ять, якою потрібно керувати.

Мабуть, це виглядає жахливо для когось із вас, але, як я вже сказав, WIP. Я дуже відкритий до конструктивної критики! Вибачте за дуже довге тіло ...


Модуль класу кісток :

Option Explicit

Private Sides() As String

Sub NewDie(NewLetters As String)
    Sides = Split(NewLetters, ",")
End Sub

Property Get Side(i As Integer)
    Side = Sides(i)
End Property

Модуль класу дерева :

Option Explicit

Private zzroot As TreeNode


Sub AddtoTree(ByVal TreeWord As Variant)
Dim i As Integer
Dim TempNode As TreeNode

    Set TempNode = TraverseTree(TreeWord, zzroot)
    SetNode TreeWord, TempNode

End Sub

Private Function SetNode(ByVal Value As Variant, parent As TreeNode) As TreeNode
Dim ValChar As String
    If Len(Value) > 0 Then
        ValChar = Left(Value, 1)
        Select Case Asc(ValChar) - 96
            Case 1:
                Set parent.Node01 = AddNode(ValChar, parent.Node01)
                Set SetNode = parent.Node01
            Case 2:
                Set parent.Node02 = AddNode(ValChar, parent.Node02)
                Set SetNode = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set parent.Node26 = AddNode(ValChar, parent.Node26)
                Set SetNode = parent.Node26
            Case Else:
                Set SetNode = Nothing
        End Select

        Set SetNode = SetNode(Right(Value, Len(Value) - 1), SetNode)
    Else
        Set parent.Node27 = AddNode(True, parent.Node27)
        Set SetNode = parent.Node27
    End If
End Function

Function AddNode(ByVal Value As Variant, NewNode As TreeNode) As TreeNode
    If NewNode Is Nothing Then
        Set AddNode = New TreeNode
        AddNode.Value = Value
    Else
        Set AddNode = NewNode
    End If
End Function
Function TraverseTree(TreeWord As Variant, parent As TreeNode) As TreeNode
Dim Node As TreeNode
Dim ValChar As String
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            Set TraverseTree = TraverseTree(Right(TreeWord, Len(TreeWord) - 1), Node)
            If Not TraverseTree Is Nothing Then
                Set TraverseTree = parent
            End If
        Else
            Set TraverseTree = parent
        End If
    Else
        If parent.Node27.Value Then
            Set TraverseTree = parent
        Else
            Set TraverseTree = Nothing
        End If
    End If
End Function

Function WordScore(TreeWord As Variant, Step As Integer, Optional parent As TreeNode = Nothing) As Integer
Dim Node As TreeNode
Dim ValChar As String
    If parent Is Nothing Then Set parent = zzroot
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            WordScore = WordScore(Right(TreeWord, Len(TreeWord) - 1), Step + 1, Node)
        End If
    Else
        If parent.Node27 Is Nothing Then
            WordScore = 0
        Else
            WordScore = Step
        End If
    End If
End Function

Function ValidWord(TreeWord As Variant, Optional parent As TreeNode = Nothing) As Integer
Dim Node As TreeNode
Dim ValChar As String
    If parent Is Nothing Then Set parent = zzroot
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            ValidWord = ValidWord(Right(TreeWord, Len(TreeWord) - 1), Node)
        Else
            ValidWord = False
        End If
    Else
        If parent.Node27 Is Nothing Then
            ValidWord = False
        Else
            ValidWord = True
        End If
    End If
End Function

Private Sub Class_Initialize()
    Set zzroot = New TreeNode
End Sub

Private Sub Class_Terminate()
    Set zzroot = Nothing
End Sub

Модуль класу TreeNode :

Option Explicit

Public Value As Variant
Public Node01 As TreeNode
Public Node02 As TreeNode
' ... - Reduced to limit size of answer.
Public Node26 As TreeNode
Public Node27 As TreeNode

Основний модуль :

Option Explicit

Const conAllSides As String = ";a,a,e,e,g,n;e,l,r,t,t,y;a,o,o,t,t,w;a,b,b,j,o,o;e,h,r,t,v,w;c,i,m,o,t,u;d,i,s,t,t,y;e,i,o,s,s,t;d,e,l,r,v,y;a,c,h,o,p,s;h,i,m,n,qu,u;e,e,i,n,s,u;e,e,g,h,n,w;a,f,f,k,p,s;h,l,n,n,r,z;d,e,i,l,r,x;"
Dim strBoard As String, strBoardTemp As String, strWords As String, strWordsTemp As String
Dim CheckWordSub As String
Dim iScore As Integer, iScoreTemp As Integer
Dim Board(1 To 4, 1 To 4) As Integer
Dim AllDice(1 To 16) As Dice
Dim AllWordsTree As Tree
Dim AllWords As Scripting.Dictionary
Dim CurWords As Scripting.Dictionary
Dim FullWords As Scripting.Dictionary
Dim JunkWords As Scripting.Dictionary
Dim WordPrefixes As Scripting.Dictionary
Dim StartTime As Date, StopTime As Date
Const MAX_LENGTH As Integer = 5
Dim Points(3 To 8) As Integer

Sub Boggle()
Dim DiceSetup() As String
Dim i As Integer, j As Integer, k As Integer

    StartTime = Now()

    strBoard = vbNullString
    strWords = vbNullString
    iScore = 0

    ReadWordsFileTree

    DiceSetup = Split(conAllSides, ";")

    For i = 1 To 16
        Set AllDice(i) = New Dice
        AllDice(i).NewDie "," & DiceSetup(i)
    Next i

    Do While WithinTimeLimit

        Shuffle

        strBoardTemp = vbNullString
        strWordsTemp = vbNullString
        iScoreTemp = 0

        FindWords

        If iScoreTemp > iScore Or iScore = 0 Then
            iScore = iScoreTemp
            k = 1
            For i = 1 To 4
                For j = 1 To 4
                    strBoardTemp = strBoardTemp & AllDice(k).Side(Board(j, i)) & "  "
                    k = k + 1
                Next j
                strBoardTemp = strBoardTemp & vbNewLine
            Next i
            strBoard = strBoardTemp
            strWords = strWordsTemp

        End If

    Loop

    Debug.Print strBoard
    Debug.Print strWords
    Debug.Print iScore & " points"

    Set AllWordsTree = Nothing
    Set AllWords = Nothing
    Set CurWords = Nothing
    Set FullWords = Nothing
    Set JunkWords = Nothing
    Set WordPrefixes = Nothing

End Sub

Sub ShuffleBoard()
Dim i As Integer

    For i = 1 To 16
        If Not WithinTimeLimit Then Exit Sub
        Board(Int((i - 1) / 4) + 1, 4 - (i Mod 4)) = Int(6 * Rnd() + 1)
    Next i

End Sub

Sub Shuffle()
Dim n As Long
Dim Temp As Variant
Dim j As Long

    Randomize
    ShuffleBoard
    For n = 1 To 16
        If Not WithinTimeLimit Then Exit Sub
        j = CLng(((16 - n) * Rnd) + n)
        If n <> j Then
            Set Temp = AllDice(n)
            Set AllDice(n) = AllDice(j)
            Set AllDice(j) = Temp
        End If
    Next n

    Set FullWords = New Scripting.Dictionary
    Set CurWords = New Scripting.Dictionary
    Set JunkWords = New Scripting.Dictionary

End Sub

Sub ReadWordsFileTree()
Dim FSO As New FileSystemObject
Dim FS
Dim strTemp As Variant
Dim iLength As Integer
Dim StartTime As Date

    StartTime = Now()
    Set AllWordsTree = New Tree
    Set FS = FSO.OpenTextFile("P:\Personal\english.txt")

    Points(3) = 1
    Points(4) = 1
    Points(5) = 2
    Points(6) = 3
    Points(7) = 5
    Points(8) = 11

    Do Until FS.AtEndOfStream
        strTemp = FS.ReadLine
        If strTemp = LCase(strTemp) Then
            iLength = Len(strTemp)
            iLength = IIf(iLength > 8, 8, iLength)
            If InStr(strTemp, "'") < 1 And iLength > 2 Then
                AllWordsTree.AddtoTree strTemp
            End If
        End If
    Loop
    FS.Close

End Sub

Function GetScoreTree() As Integer
Dim TempScore As Integer

    If Not WithinTimeLimit Then Exit Function

    GetScoreTree = 0

    TempScore = AllWordsTree.WordScore(CheckWordSub, 0)
    Select Case TempScore
        Case Is < 3:
            GetScoreTree = 0
        Case Is > 8:
            GetScoreTree = 11
        Case Else:
            GetScoreTree = Points(TempScore)
    End Select

End Function

Sub SubWords(CheckWord As String)
Dim CheckWordScore As Integer
Dim k As Integer, l As Integer

    For l = 0 To Len(CheckWord) - 3
        For k = 1 To Len(CheckWord) - l
            If Not WithinTimeLimit Then Exit Sub

            CheckWordSub = Mid(CheckWord, k, Len(CheckWord) - ((k + l) - 1))

            If Len(CheckWordSub) >= 3 And Not CurWords.Exists(CheckWordSub) Then
                CheckWordScore = GetScoreTree

                If CheckWordScore > 0 Then
                    CurWords.Add CheckWordSub, CheckWordSub
                    iScoreTemp = iScoreTemp + CheckWordScore
                    strWordsTemp = strWordsTemp & CheckWordSub & vbNewLine
                End If

                If Left(CheckWordSub, 1) = "q" Then
                    k = k + 1
                End If
            End If

        Next k
    Next l

End Sub

Sub FindWords()
Dim CheckWord As String
Dim strBoardLine(1 To 16) As String
Dim Used(1 To 16) As Boolean
Dim i As Integer, j As Integer, k As Integer, l As Integer, m As Integer, n As Integer
Dim StartSquare As Integer
Dim FullCheck As Variant

    n = 1
    For l = 1 To 4
        For m = 1 To 4
            If Not WithinTimeLimit Then Exit Sub
            strBoardLine(n) = AllDice(n).Side(Board(m, l))
            n = n + 1
        Next m
    Next l

    For i = 1 To 16
        For k = 1 To 16

            If Not WithinTimeLimit Then Exit Sub
            If k Mod 2 = 0 Then
                For j = 1 To 16
                    Used(j) = False
                Next j

                Used(i) = True
                MakeWords strBoardLine, Used, i, k / 2, strBoardLine(i)
            End If

        Next k
    Next i

    For Each FullCheck In FullWords.Items
        SubWords CStr(FullCheck)
    Next FullCheck

End Sub

Function MakeWords(BoardLine() As String, Used() As Boolean, _
    Start As Integer, _
    Direction As Integer, CurString As String) As String
Dim i As Integer, j As Integer, k As Integer, l As Integer

    j = 0

    Select Case Direction
        Case 1:
            k = Start - 5
        Case 2:
            k = Start - 4
        Case 3:
            k = Start - 3
        Case 4:
            k = Start - 1
        Case 5:
            k = Start + 1
        Case 6:
            k = Start + 3
        Case 7:
            k = Start + 4
        Case 8:
            k = Start + 5
    End Select

    If k >= 1 And k <= 16 Then
        If Not WithinTimeLimit Then Exit Function

        If Not Used(k) Then
            If ValidSquare(Start, k) Then
                If Not (JunkWords.Exists(CurString & BoardLine(k))) And Not FullWords.Exists(CurString & BoardLine(k)) Then
                    Used(k) = True
                    For l = 1 To MAX_LENGTH
                        If Not WithinTimeLimit Then Exit Function
                        MakeWords = CurString & BoardLine(k)
                        If Not (JunkWords.Exists(MakeWords)) Then
                            JunkWords.Add MakeWords, MakeWords
                        End If
                        If Len(MakeWords) = MAX_LENGTH And Not FullWords.Exists(MakeWords) Then
                            FullWords.Add MakeWords, MakeWords
                        ElseIf Len(MakeWords) < MAX_LENGTH Then
                            MakeWords BoardLine, Used, k, l, MakeWords
                        End If
                    Next l
                    Used(k) = False
                End If
            End If
        End If
    End If

    If Len(MakeWords) = MAX_LENGTH And Not FullWords.Exists(MakeWords) Then
        FullWords.Add MakeWords, MakeWords
        Debug.Print "FULL - " & MakeWords
    End If

End Function

Function ValidSquare(StartSquare As Integer, EndSquare As Integer) As Boolean
Dim sx As Integer, sy As Integer, ex As Integer, ey As Integer

    If Not WithinTimeLimit Then Exit Function

    sx = (StartSquare - 1) Mod 4 + 1
    ex = (EndSquare - 1) Mod 4 + 1

    sy = Int((StartSquare - 1) / 4 + 1)
    ey = Int((EndSquare - 1) / 4 + 1)

    ValidSquare = (sx - 1 <= ex And sx + 1 >= ex) And (sy - 1 <= ey And sy + 1 >= ey) And StartSquare <> EndSquare

End Function

Function WithinTimeLimit() As Boolean
    StopTime = Now()
    WithinTimeLimit = (Round(CDbl(((StopTime - StartTime) - Int(StopTime - StartTime)) * 86400), 0) < 120)
End Function

2
Я не переглянув код, але 50 балів насмішно мало. Я грав у довільно генерованих дошках з балами понад 1000 (використовуючи SOWPODS - список слів, що надається, може бути менш обширним). Можливо, ви хочете перевірити наявність помилки в знаку!
Пітер Тейлор

@PeterTaylor Дякую за пропозицію. Я знаю, що оцінка дуже занижена, і я знаю, що частина проблеми полягає в тому, що я бачу пропущені очевидні слова ...
Gaffi

@PeterTaylor Крім того, для запису я постійно публікую свій прогрес, а не чекаю свого остаточного продукту, оскільки ніхто ще не вбив його. Я хотів би зберегти питання дещо живим, поки це не відбудеться.
Гаффі

Я також повинен зазначити, що це не запускається на найшвидшій машині там, і це теж не допомагає.
Гаффі

1
@Gaffi 10 секунд, щоб обчислити рахунок? Це 9,999 секунди занадто довго. Ви повинні переосмислити свій код. Якщо ви відмовитесь перетворювати свій список слів на дерево, то принаймні зробіть це: Створіть список (двосторонні хештелі, будь-які) всіх двобуквенних префіксів. Потім, коли ви почнете слідувати шляху на дошці, якщо перші дві літери відсутні у списку, пропустіть ціле піддірево можливих шляхів. Знову ж таки, найкраще побудувати повне дерево, але двобуквенний список префіксів допоможе і зробити його дуже дешево.
хлібопічка

2

Швидкий огляд розміру простору пошуку.

   16! => 20922789888000 Dice Permutations
(6^16) =>  2821109907456 Face Permutations
 59025489844657012604928000 Boggle Grids 

Зменшення, щоб виключити повторення на кожній штампі.

              16! => 20922789888000 Dice Permutations
(4^4)*(5^6)*(6^5) => 31104000000 Unique Face Permutations
   650782456676352000000000 Boggle Grids 

@breadbox зберігає словник як перевірку хеш-таблиць O (1).

EDIT

Найкраща рада (я була свідком поки що)

L  E  A  N
S  E  T  M
T  S  B  D
I  E  G  O

Score: 830
Words: 229
SLEETIEST  MANTELETS
MANTEELS  MANTELET  MATELESS
MANTEEL  MANTELS  TESTEES  BETISES  OBTESTS  OBESEST
SLEETS  SLEEST  TESTIS  TESTES  TSETSE  MANTES  MANTEL  TESTAE  TESTEE
STEELS  STELES  BETELS  BESETS  BESITS  BETISE  BODGES  BESEES  EISELS
GESTES  GEISTS  OBTEST
LEANT  LEATS  LEETS  LEESE  LESES  LESTS  LESBO  ANTES  NATES  SLEET  SETAE
SEATS  STIES  STEEL  STETS  STEAN  STEAM  STELE  SELES  TAELS  TEELS  TESTS
TESTE  TELES  TETES  MATES  TESTA  TEATS  SEELS  SITES  BEETS  BETEL  BETES
BESET  BESTS  BESIT  BEATS  BODGE  BESEE  DOGES  EISEL  GESTS  GESTE  GESSE
GEITS  GEIST  OBESE
LEAN  LEAT  LEAM  LEET  LEES  LETS  LEST  LESS  EATS  EELS  ELSE  ETNA  ESES
ESTS  ESSE  ANTE  ANTS  ATES  AMBO  NATS  SLEE  SEEL  SETA  SETS  SESE  SEAN
SEAT  SEAM  SELE  STIE  STET  SEES  TAEL  TAES  TEEL  TEES  TEST  TEAM  TELE
TELS  TETS  TETE  MATE  MATS  MAES  TIES  TEAT  TEGS  SELS  SEGO  SITS  SITE
BEET  BEES  BETA  BETE  BETS  BEST  BEAN  BEAT  BEAM  BELS  BOGS  BEGO  BEGS
DOGE  DOGS  DOBS  GOBS  GEST  GEIT  GETS  OBES
LEA  LEE  LET  LES  EAN  EAT  EEL  ELS  ETA  EST  ESS  ANT  ATE  NAT  NAE  NAM
SEE  SET  SEA  SEL  TAN  TAE  TAM  TEE  TES  TEA  TEL  TET  MNA  MAN  MAT  MAE
TIE  TIS  TEG  SEG  SEI  SIT  BEE  BET  BEL  BOD  BOG  BEG  DOG  DOB  ITS  EGO
GOD  GOB  GET  OBS  OBE
EA  EE  EL  ET  ES  AN  AT  AE  AM  NA  ST  TA  TE  MA
TI  SI  BE  BO  DO  IT  IS  GO  OD  OB

Забери мені машину з стільки оперативної пам’яті, і ми поговоримо.
хлібниця

Вам потрібно розділити перестановки з кістки на 8, щоб врахувати симетрію квадрата. Крім того, як ви отримуєте (4 ^ 4) (5 ^ 6) (6 ^ 5)? Я роблю це (4 ^ 3) (5 ^ 7) (6 ^ 6), для загального простору пошуку трохи більше 2 ^ 79.
Пітер Тейлор

@ Петер Тейлор: Ти маєш рацію. Я, мабуть, видалив один із багатьох, коли роблять унікальні обличчя. Я думаю, що ми можемо погодитися, що це 83 унікальні обличчя (за винятком повторів через штамп). Виберіть будь-які 16 без повторів. '83 x 82 x 81 x 80 x 79 x 78 x 77 x 76 x 75 x 74 x 73 x 72 x 71 x 70 x 69 x 68 'Приблизно: 1.082 x (10 ^ 30) ==> ~ 2 ^ 100 Що коли-небудь це є, його велика кількість.
Адам Speight

2
@AdamSpeight Я спочатку припускав, що ваш коментар щодо збереження словника як хештеля був лише жартом, і я в основному проігнорував його. Мої вибачення. Правильною відповіддю було б: насправді хештел - це хитра структура даних для цієї проблеми. Він може відповісти лише на питання "чи дійсне слово X?", Тому для того, щоб знайти слова, вам потрібно побудувати всі можливі рядки. DAWG дозволяє запитати "чи X - це префікс будь-якого дійсного слова? І якщо так, то які літери можуть слідувати за ним?" Це дозволяє обрізати пошуковий простір на крихітну дрібну частину його загального розміру.
хлібопічка

Хештеб - це жахливо, оскільки він заважає вам вибивати фрагменти слів, які ніколи не стануть повноцінними словами (dicttree.ceiling (фрагмент) .startsWith (фрагмент)). Хоча на будь-якій дошці шахрайства є багато мільйонів потенційних слів, ви можете викинути величезну частину з них після того, як 2-3 букви будуть нанизані разом. Обхід дерева проходить повільніше, ніж пошук хешбелів, але дерево дозволяє пройти 99+ відсотків роботи за допомогою зворотного відстеження.
Jim W

1

Мій запис тут на Dream.In.Code ~ 30 мс за пошук на дошці (на 2-х основній машині має бути швидше з більшою кількістю ядер)


Ви все ще переглядаєте це, але у вашому першому посиланні на цій сторінці відсутнє :в http://. ;-)
Гаффі

Дуже хороша. Я спробую вкрасти це для себе як досвід навчання. .NETщоб VBAне надто важко
Гаффі

Чи не заперечуєте ви оновити відповідь, щоб включити середній бал під час запуску списку ISPELL (а не SOWPODS)? Це є складною задачею, і мені цікаво побачити, як ваші результати порівнюються з хлібопекарськими.
Гаффі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.