Бърз алгоритъм за поставяне на блокове, необходим е съвет?

Трябва да емулирам стратегията за разположение на прозореца на мениджъра на прозорци Fluxbox.

Като грубо ръководство, визуализирайте прозорци с произволен размер, запълващи екрана един по един, като грубият размер на всеки води до средно 80 прозореца на екрана, без нито един прозорец да се припокрива с друг.

Ако имате инсталирани Fluxbox и Xterm на вашата система, можете да опитате xwinmidiarptoy BASH скрипт, за да видя груб прототип на това, което искам да се случи. Вижте бележките xwinmidiarptoy.txt, които имах написано за него, обясняващо какво прави и как трябва да се използва.

Важно е да се отбележи, че прозорците ще се затворят и пространството, което затворените прозорци са заемали преди, става отново достъпно за поставяне на нови прозорци.

Алгоритъмът трябва да бъде Онлайн алгоритъм, обработващ данните "на части -част по сериен начин, т.е. в реда, в който входът се подава към алгоритъма, без целият вход да е наличен от самото начало."

Стратегията за поставяне на прозорец на Fluxbox има три бинарни опции, които искам да емулирам:

  • Windows изгражда хоризонтални редове или вертикални колони (потенциално)

  • Прозорците се поставят отляво надясноили отдясно наляво

  • Прозорците се поставят отгоре надолуили отдолу нагоре

Разлики между целевия алгоритъм и алгоритъма за поставяне на прозорец

Координатните единици не са пиксели. Решетката, в която ще бъдат поставени блоковете, ще бъде 128 x 128 единици. Освен това зоната за поставяне може да бъде допълнително свита от гранична зона, поставена в мрежата.

Защо алгоритъмът е проблем?

Трябва да работи в съответствие със сроковете на нишка в реално време в аудио приложение.

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

И въпреки че алгоритъмът никога няма да постави прозорец, който припокрива друг, потребителят ще може да поставя и премества определени типове блокове, припокриващи се прозорци ще съществуват. Структурата на данните, използвана за съхраняване на прозорците и/или свободното пространство, трябва да може да се справи с това припокриване.

Досега имам два избора, за които съм създал свободни прототипи:

1) Порт на алгоритъма за поставяне на Fluxbox в моя код.

Проблемът с това е, че клиентът (моята програма) се изхвърля от аудио сървъра (JACK), когато се опитам да поставя най-лошият сценарий от 256 блока, използвайки алгоритъма. Този алгоритъм извършва над 14 000 пълни (линейни) сканирания на списъка с вече поставени блокове при поставянето на 256-ия прозорец.

За демонстрация на това създадох програма, наречена text_boxer-0.0.2.tar.bz2, който приема текстов файл като вход и го подрежда в ASCII полета. Издаване make, за да го изградите. Малко недружелюбно, използвайте --help (или друга невалидна опция) за списък с опции на командния ред. Трябва да посочите текстовия файл, като използвате опцията.

2) Моят алтернативен подход.

Приложен само частично, този подход използва структура от данни за всяка област от правоъгълно свободно неизползвано пространство (списъкът с прозорци може да бъде напълно отделен и не е необходим за тестване на този алгоритъм). Структурата на данните действа като възел в двойно свързан списък (със сортирано вмъкване), както и като съдържа координатите на горния ляв ъгъл, както и ширината и височината.

Освен това всяка блокова структура от данни също съдържа четири връзки, които се свързват с всеки непосредствено съседен (докосващ се) блок от всяка от четирите страни.

ВАЖНО ПРАВИЛО: Всеки блок може да се докосва само с един блок на страна. Това е правило, специфично за начина на алгоритъма за съхраняване на свободно неизползвано пространство и не влияе върху това колко действителни прозореца могат да се допират един до друг.

Проблемът с този подход е, че е много сложен. Приложих простите случаи, при които 1) пространството се премахва от единия ъгъл на блок, 2) разделям съседните блокове, така че да се спазва ВАЖНОТО ПРАВИЛО.

По-малко лесният случай, при който пространството, което трябва да бъде премахнато, може да бъде намерено само в рамките на колона или ред от кутии, се прилага само частично - ако един от блоковете, които трябва да бъдат премахнати, отговаря точно на ширина (т.е. колона) или височина (т.е. ред), тогава възникват проблеми. И дори не споменавайте факта, че това проверява само колони с ширина на едно поле и редове с височина на едно поле.

Приложих този алгоритъм в C - езикът, който използвам за този проект (не съм използвал C++ от няколко години и ми е неудобно да го използвам, след като съсредоточих цялото си внимание върху разработката на C, това е хоби). Реализацията е 700+ реда код (включително много празни редове, скоби, коментари и т.н.). Внедряването работи само за стратегията за поставяне на хоризонтални редове + ляво-дясно + горе-долу.

Така че или трябва да добавя някакъв начин да накарам тези +700 реда код да работят за другите 7 опции за стратегия за разположение, или ще трябва да дублирам тези +700 реда код за останалите седем опции. Нито едно от тях не е привлекателно, първото, защото съществуващият код е достатъчно сложен, второто, поради раздуването.

Алгоритъмът дори не е на етап, в който мога да го използвам в най-лошия сценарий в реално време поради липсваща функционалност, така че все още не знам дали действително се представя по-добре или по-лошо от първия подход.

Текущото състояние на изпълнението на C на този алгоритъм е freespace.c. Използвам gcc -O0 -ggdb freespace.c за изграждане и го стартирам в xterm с размер най-малко 124 x 60 знака.

Какво друго има?

Прегледах набързо и отстъпих:

  • Алгоритми за опаковане в контейнери: техният акцент върху оптималното прилягане не съответства на изискванията на този алгоритъм.

  • Алгоритми за рекурсивно разполовяване: звучи обещаващо, но те са за проектиране на верига. Техният акцент е оптималната дължина на проводника.

И при двете, особено при последната, всички елементи, които трябва да бъдат поставени/пакети, са известни преди алгоритъма да започне.

Какво мислите по този въпрос? Как бихте подходили? Какви други алгоритми трябва да гледам? Или дори какви концепции трябва да проуча, тъй като не съм учил компютърни науки/софтуерно инженерство?

Моля, задавайте въпроси в коментарите, ако е необходима допълнителна информация.

Допълнителни идеи, развити след задаването на този въпрос

  • Някаква комбинация от моя „алтернативен алгоритъм“ с пространствена хеш карта за идентифициране дали голям прозорец, който трябва да бъде поставен, ще покрие няколко блока свободно пространство.

person Community    schedule 30.04.2010    source източник
comment
О, защо настоявам да задавам толкова сложни въпроси в петък вечер (местно време), когато всеки има по-добри неща за вършене????   -  person James Morris    schedule 30.04.2010
comment
Това са единствените интересни въпроси на SO! Ежедневният поток от тривиални въпроси е това, което ме кара да не искам да посещавам този сайт.   -  person Victor Liu    schedule 30.04.2010
comment
@James Morris: Имам най-странното чувство, че съм чел по-кратка версия на този въпрос преди...   -  person ire_and_curses    schedule 30.04.2010
comment
@ire_and_curses: Да, продължавам да задавам много подобни въпроси, не получавам отговори, след което ги изтривам.   -  person James Morris    schedule 30.04.2010
comment
@James Morris: Прозорците трябва ли да бъдат подравнени към мрежата 127x127 или могат да заемат, да речем, половин клетка от мрежата? Какво е разпределението на размерите на прозорците?   -  person Victor Liu    schedule 30.04.2010
comment
Няма половин клетки не. Разпределението на размерите на прозореца, най-малкият трябва да е 1x1, най-големият... Не съм сигурен, може би 64x64?   -  person James Morris    schedule 30.04.2010
comment
Ако целият ви домейн е само 127x127, защо просто не използвате ефективно растерно изображение? Просто запишете кои клетки от мрежата са заети и кои не. Преглеждането на цялото нещо не може да отнеме много време. Колко бързо трябва да можете да добавяте/премахвате прозорци?   -  person Victor Liu    schedule 30.04.2010
comment
@Victor: Ах, тук става сложно, защото въпреки че алгоритъмът за поставяне не трябва никога да припокрива прозорци, ще има определени ситуации, благодарение на нашия скъп приятел потребителя, в които прозорците да се припокриват.   -  person James Morris    schedule 30.04.2010
comment
@James: Така че, ако вместо да следите дали блоковете са заети, вие просто съхранявате броя на прозорците, използващи този блок. Можете ли да актуализирате това число всеки път, когато потребителят премести прозорец? Освен това за какво лудо приложение е това?!   -  person Victor Liu    schedule 30.04.2010
comment
Ритмичен модел ще задейства поставянето на кутии. Координатите x и y на всяко поставено поле ще определят данните за височината и скоростта на тригера и след това MIDI съобщение ще бъде изведено към JACK - т.е. планира се да бъде някакъв MIDI секвенсер/арпегиатор. Междувременно появата/изчезването на всички тези прозорци ще хипнотизира потребителя!   -  person James Morris    schedule 30.04.2010
comment
Вашият алгоритъм има ли право да препозиционира съществуващи прозорци? Това ще бъде ли твърде объркващо за потребителя?   -  person outis    schedule 01.05.2010
comment
@outis: не, алгоритъмът не може да препозиционира прозорци.   -  person James Morris    schedule 01.05.2010


Отговори (3)


Бих обмислил някакъв вид структура за пространствено хеширане. Представете си, че цялото ви свободно пространство е грубо очертано, наречете ги блокове. Докато прозорците идват и си отиват, те заемат определени комплекти от съседни правоъгълни блокове. За всеки блок следете най-големия неизползван правоъгълник, падащ на всеки ъгъл, така че трябва да съхранявате 2*4 реални числа на блок. За празен блок правоъгълниците във всеки ъгъл имат размер, равен на блока. По този начин един блок може да бъде „използван“ само в ъглите си и така най-много 4 прозореца могат да стоят във всеки блок.

Сега всеки път, когато добавите прозорец, трябва да търсите правоъгълен набор от блокове, за които прозорецът ще пасне, и когато го направите, актуализирайте свободните размери на ъглите. Трябва да оразмерите блоковете си така, че шепа (~4x4) от тях да се поберат в типичен прозорец. За всеки прозорец следете кои блокове докосва (трябва да следите само екстентите), както и кои прозорци докосват даден блок (най-много 4, в този алгоритъм). Има очевиден компромис между детайлността на блоковете и количеството работа за вмъкване/премахване на прозорец.

Когато премахвате прозорец, преминете през всички блокове, които той докосва, и за всеки блок преизчислете размерите на свободните ъгли (знаете кои прозорци го докосват). Това е бързо, тъй като вътрешният цикъл е най-много с дължина 4.

Представям си структура от данни като

struct block{
    int free_x[4]; // 0 = top left, 1 = top right,
    int free_y[4]; // 2 = bottom left, 3 = bottom right
    int n_windows; // number of windows that occupy this block
    int window_id[4]; // IDs of windows that occupy this block
};
block blocks[NX][NY];

struct window{
    int id;
    int used_block_x[2]; // 0 = first index of used block,
    int used_block_y[2]; // 1 = last index of used block
};

Редактиране

Ето снимка:

алтернативен текст

Той показва два примерни блока. Цветните точки показват ъглите на блока, а стрелките, излизащи от тях, показват обхвата на най-големия свободен правоъгълник от този ъгъл.

Вие споменахте в редакцията, че мрежата, върху която ще бъдат поставени вашите прозорци, вече е доста груба (127x127), така че размерите на блоковете вероятно ще бъдат нещо като 4 клетки на мрежата на една страна, което вероятно няма да ви спечели много. Този метод е подходящ, ако координатите на ъгъла на вашия прозорец могат да приемат много стойности (мислех, че ще бъдат пиксели), но не толкова във вашия случай. Все още можете да го опитате, тъй като е просто. Вероятно бихте искали също така да запазите списък с напълно празни блокове, така че ако се появи прозорец, който е по-голям от 2 ширини на блока, тогава да погледнете първо в този списък, преди да потърсите подходящо свободно място в блоковата мрежа.

person Victor Liu    schedule 30.04.2010
comment
+1 от мен, Виктор, колкото и странно да звучи, не очаквах съвсем различен подход. Много е интересно, просто още не го разбирам съвсем. Вижте моята редакция, в която се споменават единици, колко грубо е свободното пространство в решетка? Има ли шанс за диаграма? - person James Morris; 30.04.2010

След няколко фалстарта, в крайна сметка пристигнах тук. Тук е изоставено използването на структури от данни за съхраняване на правоъгълни области на свободно пространство. Вместо това има 2d масив с 128 x 128 елемента за постигане на същия резултат, но с много по-малко сложност.

Следната функция сканира масива за площ с размер width * height. Първата позиция, която намира, записва горните леви координати, където сочат resultx и resulty.

_Bool freespace_remove( freespace* fs,
                        int width,     int height,
                        int* resultx,  int* resulty)
{
    int x = 0;
    int y = 0;
    const int rx = FSWIDTH - width;
    const int by = FSHEIGHT - height;

    *resultx = -1;
    *resulty = -1;

    char* buf[height];

    for (y = 0; y < by; ++y)
    {
        x = 0;
        char* scanx = fs->buf[y];

        while (x < rx)
        {
            while(x < rx && *(scanx + x))
                ++x;

            int w, h;

            for (h = 0; h < height; ++h)
                buf[h] = fs->buf[y + h] + x;

            _Bool usable = true;
            w = 0;

            while (usable && w < width)
            {
                h = 0;
                while (usable && h < height)
                    if (*(buf[h++] + w))
                        usable = false;
                ++w;
            }

            if (usable)
            {
                for (w = 0; w < width; ++w)
                    for (h = 0; h < height; ++h)
                        *(buf[h] + w) = 1;

                *resultx = x;
                *resulty = y;
                return true;
            }

            x += w;
        }
    }

    return false;
}

2d масивът е инициализиран с нула. Всички области в масива, където се използва пространството, са зададени на 1. Тази структура и функция ще работят независимо от действителния списък с прозорци, които заемат областите, маркирани с 1.

Предимствата на този метод са неговата простота. Той използва само една структура от данни - масив. Функцията е кратка и не би трябвало да е твърде трудна за адаптиране, за да се справи с останалите опции за разположение (тук обработва само интелигентен ред + отляво надясно + отгоре надолу).

Първоначалните ми тестове също изглеждат обещаващи по отношение на скоростта. Въпреки че не мисля, че това би било подходящо за мениджър на прозорци, който поставя прозорци на, например, 1600 x 1200 работен плот с точност на пикселите, за моите цели вярвам, че ще бъде много по-добър от всеки от предишните методи, които имам опитах.

Компилируем тестов код тук: http://jwm-art.net/art/text/freespace_grid.c
(в Linux използвам gcc -ggdb -O0 freespace_grid.c за компилиране)

person Community    schedule 03.05.2010
comment
Ще се опитам да използвам отделни битове, а не цели знаци за съхраняване, ако се използва клетка или не. - person James Morris; 04.05.2010
comment
Все още работя върху използването на всеки отделен бит от 2d масива за представяне дали всяка клетка е заета от прозорец или не. Това означава, че 2d масивът вече е buf_type buf[128][128 / (sizeof(buf_type) * CHAR_BIT)] - да, всеки отделен елемент в 2d масива е тип с дефиниран тип, така че мога да тествам използването на char срещу int, short, long и да видя как те влияят върху производителността. След като работя специфичната реализация на row-smart+top-left+top-bottom, ще я публикувам като друг отговор с подробности за това как се сравнява с това (много) по-просто решение. - person James Morris; 08.05.2010

#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>


#define FSWIDTH 128
#define FSHEIGHT 128


#ifdef USE_64BIT_ARRAY
    #define FSBUFBITS 64
    #define FSBUFWIDTH 2
    typedef uint64_t fsbuf_type;
    #define TRAILING_ZEROS( v ) __builtin_ctzl(( v ))
    #define LEADING_ONES( v )   __builtin_clzl(~( v ))
#else
#ifdef USE_32BIT_ARRAY
    #define FSBUFBITS 32
    #define FSBUFWIDTH 4
    typedef uint32_t fsbuf_type;
    #define TRAILING_ZEROS( v ) __builtin_ctz(( v ))
    #define LEADING_ONES( v )   __builtin_clz(~( v ))
#else
#ifdef USE_16BIT_ARRAY
    #define FSBUFBITS 16
    #define FSBUFWIDTH 8
    typedef uint16_t fsbuf_type;
    #define TRAILING_ZEROS( v ) __builtin_ctz( 0xffff0000 | ( v ))
    #define LEADING_ONES( v )   __builtin_clz(~( v ) << 16)
#else
#ifdef USE_8BIT_ARRAY
    #define FSBUFBITS 8
    #define FSBUFWIDTH 16
    typedef uint8_t fsbuf_type;
    #define TRAILING_ZEROS( v ) __builtin_ctz( 0xffffff00 | ( v ))
    #define LEADING_ONES( v )   __builtin_clz(~( v ) << 24)
#else
    #define FSBUFBITS 1
    #define FSBUFWIDTH 128
    typedef unsigned char fsbuf_type;
    #define TRAILING_ZEROS( v ) (( v ) ? 0 : 1)
    #define LEADING_ONES( v )   (( v ) ? 1 : 0)
#endif
#endif
#endif
#endif


static const fsbuf_type fsbuf_max =   ~(fsbuf_type)0;
static const fsbuf_type fsbuf_high =  (fsbuf_type)1 << (FSBUFBITS - 1);


typedef struct freespacegrid
{
    fsbuf_type buf[FSHEIGHT][FSBUFWIDTH];

    _Bool left_to_right;
    _Bool top_to_bottom;

} freespace;


void freespace_dump(freespace* fs)
{
    int x, y;

    for (y = 0; y < FSHEIGHT; ++y)
    {
        for (x = 0; x < FSBUFWIDTH; ++x)
        {
            fsbuf_type i = FSBUFBITS;
            fsbuf_type b = fs->buf[y][x];

            for(; i != 0; --i, b <<= 1)
                putchar(b & fsbuf_high ? '#' : '/');
/*
            if (x + 1 < FSBUFWIDTH)
                putchar('|');
*/
        }
        putchar('\n');
    }
}


freespace* freespace_new(void)
{
    freespace* fs = malloc(sizeof(*fs));

    if (!fs)
        return 0;

    int y;

    for (y = 0; y < FSHEIGHT; ++y)
    {
        memset(&fs->buf[y][0], 0, sizeof(fsbuf_type) * FSBUFWIDTH);
    }

    fs->left_to_right = true;
    fs->top_to_bottom = true;

    return fs;
}


void freespace_delete(freespace* fs)
{
    if (!fs)
        return;

    free(fs);
}

/* would be private function: */
void fs_set_buffer( fsbuf_type buf[FSHEIGHT][FSBUFWIDTH],
                    unsigned x,
                    unsigned y1,
                    unsigned xoffset,
                    unsigned width,
                    unsigned height)
{
    fsbuf_type v;
    unsigned y;

    for (; width > 0 && x < FSBUFWIDTH; ++x)
    {
        if (width < xoffset)
            v = (((fsbuf_type)1 << width) - 1) << (xoffset - width);
        else if (xoffset < FSBUFBITS)
            v = ((fsbuf_type)1 << xoffset) - 1;
        else
            v = fsbuf_max;

        for (y = y1; y < y1 + height; ++y)
        {
#ifdef FREESPACE_DEBUG
            if (buf[y][x] & v)
                printf("**** over-writing area ****\n");
#endif
            buf[y][x] |= v;
        }

        if (width < xoffset)
            return;

        width -= xoffset;
        xoffset = FSBUFBITS;
    }
}


_Bool freespace_remove(   freespace* fs,
                          unsigned width, unsigned height,
                          int* resultx,   int* resulty)
{
    unsigned x, x1, y;
    unsigned w, h;
    unsigned xoffset, x1offset;
    unsigned tz; /* trailing zeros */

    fsbuf_type* xptr;
    fsbuf_type mask =   0;
    fsbuf_type v;

    _Bool scanning = false;
    _Bool offset = false;

    *resultx = -1;
    *resulty = -1;

    for (y = 0; y < (unsigned) FSHEIGHT - height; ++y)
    {
        scanning = false;
        xptr = &fs->buf[y][0];

        for (x = 0; x < FSBUFWIDTH; ++x, ++xptr)
        {
            if(*xptr == fsbuf_max)
            {
                scanning = false;
                continue;
            }

            if (!scanning)
            {
                scanning = true;
                x1 = x;
                x1offset = xoffset = FSBUFBITS;
                w = width;
            }
retry:
            if (w < xoffset)
                mask = (((fsbuf_type)1 << w) - 1) << (xoffset - w);
            else if (xoffset < FSBUFBITS)
                mask = ((fsbuf_type)1 << xoffset) - 1;
            else
                mask = fsbuf_max;

            offset = false;

            for (h = 0; h < height; ++h)
            {
                v = fs->buf[y + h][x] & mask;

                if (v)
                {
                    tz = TRAILING_ZEROS(v);
                    offset = true;
                    break;
                }
            }

            if (offset)
            {
                if (tz)
                {
                    x1 = x;
                    w = width;
                    x1offset = xoffset = tz;
                    goto retry;
                }
                scanning = false;
            }
            else
            {
                if (w <= xoffset) /***** RESULT! *****/
                {
                    fs_set_buffer(fs->buf, x1, y, x1offset, width, height);
                    *resultx = x1 * FSBUFBITS + (FSBUFBITS - x1offset);
                    *resulty = y;
                    return true;
                }
                w -= xoffset;
                xoffset = FSBUFBITS;
            }
        }
    }
    return false;
}


int main(int argc, char** argv)
{
    int x[1999];
    int y[1999];
    int w[1999];
    int h[1999];

    int i;

    freespace* fs = freespace_new();

    for (i = 0; i < 1999; ++i, ++u)
    {
        w[i] = rand() % 18 + 4;
        h[i] = rand() % 18 + 4;

        freespace_remove(fs, w[i], h[i], &x[i], &y[i]);
/*
        freespace_dump(fs);
        printf("w:%d h:%d x:%d y:%d\n", w[i], h[i], x[i], y[i]);
        if (x[i] == -1)
            printf("not removed space %d\n", i);
        getchar();
*/
    }

    freespace_dump(fs);
    freespace_delete(fs);

    return 0;
}

Кодът по-горе изисква едно от USE_64BIT_ARRAY, USE_32BIT_ARRAY, USE_16BIT_ARRAY, USE_8BIT_ARRAY да бъде дефинирано, в противен случай ще се върне към използването само на старшия бит на unsigned char за съхраняване на състоянието на клетките на мрежата.

Функцията fs_set_buffer няма да бъде декларирана в заглавката и ще стане статична в изпълнението, когато този код бъде разделен между .h и .c файлове. Ще бъде осигурена по-удобна за потребителя функция, скриваща подробностите за изпълнението, за премахване на използваното пространство от мрежата.

Като цяло това внедряване е по-бързо без оптимизация от моя предишен отговор с максимална оптимизация (използване на GCC на 64-битов Gentoo, опции за оптимизация -O0 и -O3 съответно).

По отношение на USE_NNBIT_ARRAY и различните размери на битовете, използвах два различни метода за синхронизиране на кода, които правят 1999 извиквания към freespace_remove.

Времето main() с помощта на командата Unix time (и деактивирането на всеки изход в кода) изглежда доказа, че очакванията ми са правилни - че по-големите размери на битовете са по-бързи.

От друга страна, синхронизирането на индивидуалните повиквания до freespace_remove (използване на gettimeofday) и сравняването на максималното време, заето с повикванията от 1999 г., изглежда показва, че по-малките размери на битовете са по-бързи.

Това е тествано само на 64-битова система (Intel Dual Core II).

person Community    schedule 15.05.2010