Всем привет! Недавно столкнулся с такой задачей: Есть массив, размер которого степень двойки (8, 16, 32 и т.д.). Необходимо переставить элементы массива, не меняя при этом их порядок следования, так, чтобы все элементы первой половины стали нечётными, а второй половины - чётными. При этом использовать динамическую память запрещается. Пример: Есть массив из 16 чисел: 0 1 2 3 4 5 6 7 8 9 a b c d e f Первая половина: 0 1 2 3 4 5 6 7 Вторая половина: 8 9 a b c d e f Результат перестановки: 0 8 1 9 2 a 3 b 4 c 5 d 6 e 7 f Мне удалось решить эту задачу за O(N*log2(N)). Есть ли возможность её решить за O(N)? З.Ы. Вообще, под такие вещи хотелось бы отдельный раздел WASM.ALGO или что-то вроде того.
За O(N) эту задачу можно решить при использовании дополнительного массива флагов. Сначала флаги очищаются, далее перебираем последовательно все индексы и если соответствующий флаг очищен, то с него начинаем делать перестановку помечая участвовавшие в ней элементы. Если делать без массива, то, чтобы не случилось повторной перестановки той же цепочки, начинать перестановку нужно с тех элементов которые имеют скажем максимальный индекс в цепочке. Проверить, что индекс максимальный можно предварительно сгенерировав все индексы цепочки, но это уже будет N*log(N). Про максимальные индексы точно можно сказать, что заканчиваться они обязательно должны на 0, а начинаться с самой длинной группы 1. Можно еще проверить что в индексе нет групп 1 длиннее чем в начале, а для таких же длинных уже сдвигать и проверять не получается ли большее значение.
Я, когда исследовал проблему, обнауржил несколько интересных свойств. Если битово представить значения старого и нового индекса элемента, то получим следующее: Код (Text): 0 - 0000 -> 0 - 0000 1 - 0001 -> 2 - 0010 2 - 0010 -> 4 - 0100 3 - 0011 -> 6 - 0110 4 - 0100 -> 8 - 1000 5 - 0101 -> A - 1010 6 - 0110 -> C - 1100 7 - 0111 -> E - 1110 8 - 1000 -> 1 - 0001 9 - 0001 -> 3 - 0011 A - 0010 -> 5 - 0101 B - 0011 -> 7 - 0111 C - 0100 -> 9 - 1001 D - 0101 -> B - 1011 E - 0110 -> D - 1101 F - 0111 -> F - 1111 Отсюда можно сделать выводы о свойствах: 1. Новый индекс элемента есть индекс старого элемента, который был циклически сдвинут влево на один бит. Пояснение: 0101 ROL 1 = 1010, 1111 ROL 1 = 1111 2. Отдельные элементы образуют замкнутые кольца обмена, которые можно преобразовать за операцию O(n), где n - длина кольца. Пояснение: для нашего примера существует 6 колец обмена (далее - в двоичном виде): Код (Text): A: 0000 -> 0000 B: 1111 -> 1111 C: 0101 -> 1010 -> 0101 D: 0001 -> 0010 -> 0100 -> 1000 -> 0001 E: 0011 -> 0110 -> 1100 -> 1001 -> 0011 F: 0111 -> 1110 -> 1101 -> 1011 -> 0111 3. Для того, чтобы элементы встали на свои новые места, достаточно один раз произвести операцию обмена значениями по каждому кольцу. Пояснение: Делаем замену по кольцу D: Код (Text): Исходный массив 0 1 2 3 4 5 6 7 8 9 A B C D E F По кольцу заменены элементы с номерами 1 -> 2 -> 4 -> 8 -> 1 Результат замены: 0 *8 *1 3 *2 5 6 7 *4 9 A B C D E F Сопоставляем с ожидаемым результатом: 0 8 1 9 2 a 3 b 4 c 5 d 6 e 7 f Вывод: после замены по кольцу все элементы, участвующие в кольце, встали на свои места. 4. Индекс следующего элемента в кольце обмена - результат циклического сдвига предыдущего индекса на 1 влево (следствие 1 и 2). 5. Размер кольца не превышает log2(N) - очевидное следствие из 4. 6. Количество колец обмена (если не считать кольца A и B из пункта 2) можно оценить по формуле R = floor((N - 2) / log2(N)). Пояснение: для N = 16 имеем floor((16-2)/log2(16)) = floor(3.5) = 4 кольца обмена (C, D, E, F). Для N=32 имеем floor((32-2)/log2(32)) = floor(6) = 6 колец обмена 7. Если однозначно определить формулу местонахождения хотя бы одного элемента для каждого кольца, то операцию преобразования массива можно выполнить за O(N) перестановок. Соответственно, сейчас проблемы с поиском индексов по пункту 7. Если кто знает, как это сделать - задача будет решена.
Код (C): for(int P=2;P<=30;P++){ int N=1<<P; unsigned char *a=new unsigned char[N]; for(int i=0;i<N;i++) a[i]=i; unsigned int S=0; for(int i=N/2;i<N;i+=2){ int j=i; do{ ++S; if(j&1) j+=N; j>>=1; }while(j<i); if(j>i) continue; int v=a[i]; do{ if(j&1) j+=N; j>>=1; v^=a[j]^=v^=a[j]; }while(j<i); } printf("%u/2^%d=%4.2f\n",S,P,S*1.0/N); if(P<=6){ for(int i=0;i<N;i++) printf("%4d",a[i]); if(N<16) printf("\n"); } delete[] a; } Если отбросить 3/4 всех возможных индексов которые максимальными быть не могут, а во внутреннем цикле проверять является ли выбранный индекс максимальным в цепочке, то logN итераций потребуется только для действительно максимального индекса, если мы начали со второго по величине индекса, то в среднем придётся просмотреть уже только половину цепочки. При больших размерах массива в проверочном цикле требуется порядка 10 итераций на индекс, а с учётом того что внешний цикл перебирает только четверть индексов, коэффициент получается не настолько уж диким. При 32 элементах требуется 35 итераций, при 2^16 требуется 2^17 итераций, а при 2^30 внутренний цикл выполняется 2.56*2^30 раз. Еще можно делать проверку, что в индексе нет такой же длинной группы 1 как начальная, в этом случае сразу переходим к перестановке элементов, проверку что в индексе есть группа длиннее начальной, в этом случае берём следующий индекс, иначе выполняем проверочный цикл и по нему уже решаем нужно переставлять элементы или нет.
Динамическую память, увы, нельзя использовать. У меня решение такое вышло (но это N*log2(N)): Код (C): #define RANK 4 #define ITEMS (1 << RANK) #define TOTAL (ITEMS << 1) int main() { int buf[TOTAL]; int tmp; for (size_t i=0; i<TOTAL; ++i) buf[i] = i; for (size_t r=1; r < ITEMS; r <<= 1) { size_t step = r << 1; for (size_t i=0; i<ITEMS; i += step) { int *a = &buf[i+r]; int *b = &buf[i+ITEMS]; for (size_t k=0; k<r; ++k) { tmp = *a; *(a++) = *b; *(b++) = tmp; } } printf("r=%d (step=%d):", r, step); for (size_t i=0; i<TOTAL; ++i) printf(" %02X", buf[i]); printf("\n"); } printf("Result:"); for (size_t i=0; i<TOTAL; ++i) printf(" %02X", buf[i]); printf("\n"); } P.S. Что-то форма набора текста сильно тормозит.
Код: Код (C): for(int P=1;P<=30;P++){ int N=1<<P; unsigned char *a=new unsigned char[N]; for(int i=0;i<N;i++) a[i]=i; unsigned E=0,//итераций внешнего цикла FS=0,//индекс явно не максимальный FW=0,//индекс явно максимальный C=0,//делаем дополнительную проверку CS=0,//индекс не максимальный CW=0,//индекс максимальный CI=0;//итераций для дополнительной проверки const int vs[17]={0,0,0x1,0x11,0x21,0x121,0x221,0x321,0x421,0x1421,0x2421,0x3421,0x4421,0x5421,0x6421,0x7421,0x8421}; int p=P/2; int m=N>>p; while(p){ int i=N-(N>>p); int v1=vs[p]; int v2=v1>>4; v1&=15; int v3=v2>>4; v2&=15; int v4=v3>>4; v3&=15; for(int ii=0;ii<m;i+=2,ii+=2){ ++E; int v=ii*2; v&=v<<v1; v&=v<<v2; v&=v<<v3; v&=v<<v4; if(v){ if(v&ii){ ++FS; continue; } int j=i; ++C; do{ ++CI; unsigned d=(v&-v); unsigned vv=(i/d+i*(N/d))&(N-1); if(vv>i) break; v&=v-1; }while(v); if(v){ ++CS; continue; }else ++CW; }else ++FW; int j=i; v=a[i]; do{ if(j&1) j+=N; j>>=1; v^=a[j]^=v^=a[j]; }while(j<i); } m=N>>p; --p; } double e=E?1000.0/E:0; double c=C?1000.0/C:0; printf("T=%u/2^%d, FS/FW/C=%3.0f/%3.0f/%3.0f, CS/CW=%3.0f/%3.0f, CI=%4.2f\n", E,P,FS*e,FW*e,C*e,CS*c,CW*c,C?CI*1.0/C:0.0); if(P<=8){ for(int i=0;i<N;i++) printf("%4d",a[i]); printf("\n"); } delete[] a; } Консоль: Код (Text): T=64/2^8, FS/FW/C=344/375/281, CS/CW=444/556, CI=1.28 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 102 104 106 108 110 112 114 116 118 120 122 124 126 128 130 132 134 136 138 140 142 144 146 148 150 152 154 156 158 160 162 164 166 168 170 172 174 176 178 180 182 184 186 188 190 192 194 196 198 200 202 204 206 208 210 212 214 216 218 220 222 224 226 228 230 232 234 236 238 240 242 244 246 248 250 252 254 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63 65 67 69 71 73 75 77 79 81 83 85 87 89 91 93 95 97 99 101 103 105 107 109 111 113 115 117 119 121 123 125 127 129 131 133 135 137 139 141 143 145 147 149 151 153 155 157 159 161 163 165 167 169 171 173 175 177 179 181 183 185 187 189 191 193 195 197 199 201 203 205 207 209 211 213 215 217 219 221 223 225 227 229 231 233 235 237 239 241 243 245 247 249 251 253 255 T=128/2^9, FS/FW/C=406/328/266, CS/CW=529/471, CI=1.24 ... T=268435456/2^30, FS/FW/C=811/ 98/ 91, CS/CW=608/392, CI=1.35 T=268435456/2^30 означает, что из 2^30 индексов мы проверяли только четверть. FS/FW/C=811/ 98/ 91 означает, что из 1000 случаев: 811 мы сразу откинули, в 98 перешли к перестановке, в 91 стали делать дополнительную проверку CS/CW=608/392 означает что по результатам дополнительной проверки в полтора раза чаще приходится переходит к следующему индексу, чем начинать перестановку CI=1.35 означает, что цикл дополнительной проверки в среднем выполняется чуть более одного раза. В общем можно считать, что цель достигнута и сложность почти линейная. Еще можно развернуть цикл while(p), сделав вместо него P/2 циклов for(int ii=0;ii<m;i+=2,ii+=2), внутри убрать лишние v&=v<<vх, а в оставшиеся подставить константы из таблицы, только это будет слишком много кода.
Такое вот Код (C): static void swap_values (int* a, int* b) { *a ^= *b; *b ^= *a; *a ^= *b; } static void swap_items (int* items, int count) { int src, dst, next, half, value; half = count / 2; src = 1; dst = src * 2; next = src + 2; value = items[src]; while (1) { if (dst < src) { if (dst < half) { items[dst] = value; if (next >= half) break; src = next; next += 2; dst = src * 2; value = items[src]; } else { swap_values (&value, &items[dst]); src = dst; dst -= count - dst - 1; } } else { swap_values (&value, &items[dst]); src = dst; if (dst < half) dst = src * 2; else dst -= count - dst - 1; } } } Только работает для 16 элементов, потому что рисунок с перестановками именно для них перед глазами висел. На 18 элементах "забывает" вставить один последний, надо над условиями для общего случая подумоть...
Если убрать из кода все счётчики, то получится: Код (C): for(int P=1;P<=28;P++){ unsigned N=1<<P,N1=N-1,P1=P-1,P2=P/2; unsigned *a=new unsigned [N]; for(unsigned i=0;i<N;i++) a[i]=i; unsigned T=GetTickCount(); for(unsigned i=N/2;i<N;i+=2){ int v=0; DWORD p; _BitScanReverse(&p,i^N1); p=P1-p; if(p<P2){ v=(i&(N1>>p))<<1; if(p>8){ v&=v<<(p-8); v&=v<<4; v&=v<<2; v&=v<<1; }else if(p>4){ v&=v<<(p-4); v&=v<<2; v&=v<<1; }else if(p>=2){ v&=v<<(p-2); v&=v<<1; } } if(v){ if(v&i) continue; do{ _BitScanReverse(&p,v&-v); unsigned vv=((i>>p)+(i<<(P-p)))&N1; if(vv>i) break; }while(v&=v-1); if(v) continue; } int j=i; if(j&1) j+=N; j>>=1; v=a[j]; do{ int jj=j; if(j&1) j+=N; j>>=1; a[jj]=a[j]; }while(j<i); a[j]=v; } printf("N=2^%d time=%u ms\n",P,GetTickCount()-T); if(P<=8){ for(int i=0;i<N;i++) printf("%4d",a[i]); printf("\n"); } delete[] a; } Время работы для массива слов: Код (Text): N=2^20 time=0 ms N=2^21 time=31 ms N=2^22 time=94 ms N=2^23 time=172 ms N=2^24 time=374 ms N=2^25 time=811 ms N=2^26 time=1810 ms N=2^27 time=4056 ms N=2^28 time=8955 ms Для байтового массива: Код (Text): N=2^20 time=15 ms N=2^21 time=16 ms N=2^22 time=62 ms N=2^23 time=125 ms N=2^24 time=281 ms N=2^25 time=639 ms N=2^26 time=1357 ms N=2^27 time=2995 ms N=2^28 time=6770 ms N=2^29 time=15928 ms N=2^30 time=34928 ms Если реальную перестановку не делать, а только определять с какого индекса её нужно начинать, то время выполнения уменьшается в 10-15 раз, отсюда напрашивается вывод что индексы проверяются достаточно быстро и в оптимизации нуждается сам цикл перестановок.
В некоторых случаях есть возможность перечислить все цепочки явно. Циклический сдвиг индекса эквивалентен его удвоению вятому по модулю 2^n-1, и в том случае когда 2^n-1 простое, а 2^n-2 делится на n можно просто домножая индекс на 3^n и получать элемент другой цепочки. К примеру для n=5 мы получаем 6 цепочек по 5 элементов которые можно начинать с индексов 1, 26, 25, 30, 5, 6, для n=7 имеем 18 цепочек, которые можно начинать с элементов 1, 28, 22, 108, 103, 90, 107, 75, 68, 126, 99, 105, 19, 24, 37, 20, 52, 59. А вот для n=4 или n=6 в нашем кольце появляются делители нуля и примитивный элемент, который позволил бы легко перечислить все цепочки, там уже не найти.
Такое вот Код (C): static void swap_items2 (int* items, int count) { int src, dst, first, half, value; unsigned int bitmap; half = count / 2; bitmap = 0; first = src = 1; bitmap |= 1 << (src / 2); value = items[src]; while (1) { if (src < half) { dst = src * 2; swap_values (&value, &items[dst]); src = dst; } else { dst = src - (count - src - 1); if (dst < half) { if (bitmap & (1 << (dst / 2))) { items[dst] = value; for (src=first; src < half && bitmap & (1 << (src / 2)); ) src += 2; if (src < half) { first = src; bitmap |= 1 << (src / 2); value = items[src]; } else break; } else { swap_values (&value, &items[dst]); bitmap |= (dst & 1) << (dst / 2); src = dst; } } else { swap_values (&value, &items[dst]); src = dst; } } } } Но тут дополнительная память нужна, 1 дворд на каждые 128 элементов. (Этот пример для любого четного N от 4 до 128).
Все перестановки состоят из ряда закольцованных цепочек. Любая цепочка начинается в первой половине по нечетному индексу; индекс следующего элемента цепочки равен index * 2, если он в первой половине и index - (count - index - 1), если он во второй половине. Все цепочки заканчиваются на том же элементе, с которого начались.
Начальные индексы цепочек для разных N, может зависимость какая увидится: Код (Text): N=4: 1 N=6: 1 N=8: 1 3 N=10: 1 3 N=12: 1 N=14: 1 N=16: 1 3 5 7 N=18: 1 3 N=20: 1 N=22: 1 3 5 7 9 N=24: 1 5 N=26: 1 5 N=28: 1 3 9 N=30: 1 N=32: 1 3 5 7 11 15 N=34: 1 3 5 11 N=36: 1 3 5 7 15 N=38: 1 N=40: 1 3 7 13 N=42: 1 3 N=44: 1 3 7 N=46: 1 3 5 7 9 15 21 N=48: 1 5 N=50: 1 3 7 21 N=52: 1 3 5 9 11 17 19 N=54: 1 N=56: 1 3 5 11 N=58: 1 3 5 19 N=60: 1 N=62: 1 N=64: 1 3 5 7 9 11 13 15 21 23 27 31 N=66: 1 3 5 7 11 13 N=68: 1 N=70: 1 3 5 15 23 N=72: 1 7 N=74: 1 3 5 9 11 13 17 25 N=76: 1 3 5 7 15 25 35 N=78: 1 3 7 11 33 N=80: 1 3 N=82: 1 3 9 27 N=84: 1 N=86: 1 3 5 7 9 13 15 17 21 29 37 N=88: 1 3 5 29 N=90: 1 3 5 9 11 13 19 33 N=92: 1 3 7 9 11 13 17 19 39 N=94: 1 3 5 7 9 11 15 17 21 23 31 33 45 N=96: 1 5 7 19 N=98: 1 5 N=100: 1 3 5 9 11 15 33 N=102: 1 N=104: 1 3 N=106: 1 3 5 7 9 11 13 15 17 21 25 35 45 49 N=108: 1 N=110: 1 3 9 N=112: 1 3 11 37 N=114: 1 3 5 9 N=116: 1 5 7 23 25 N=118: 1 3 5 7 9 13 17 21 25 29 39 N=120: 1 3 7 11 13 17 21 51 N=122: 1 11 N=124: 1 3 7 9 11 23 41 N=126: 1 5 25 N=128: 1 3 5 7 9 11 13 15 19 21 23 27 29 31 43 47 55 63
Ну и после того, как сам себе пояснил обобщенный алгоритм, упрощенный вариант. Код (C): static void swap_items3 (int* items, int count) { int src, dst, first, half, value; unsigned int bitmap; half = count / 2; bitmap = 0; first = src = 1; bitmap |= 1 << (src / 2); value = items[src]; while (1) { if (src < half) dst = src * 2; else dst = src - (count - src - 1); swap_values (&value, &items[dst]); src = dst; if (dst == first) { for (src=first; src < half && bitmap & (1 << (src / 2)); ) src += 2; if (src < half) { first = src; bitmap |= 1 << (src / 2); value = items[src]; } else break; } else if (dst < half) bitmap |= (dst & 1) << (dst / 2); } }
Так. Вернулся на свежую голову. Хочу отдельно поблагодарить rmn за рисунок. Я такие же рисунки рисовал, но его рисовка натолкнула меня на следующую идею. Следствие 8: Для определения, является ли номер индекса массива первым элементом цепочки обмена, достаточно расчитать предыдущий элемент цепочки обмена и сравнить с текущим значением. Если текущее значение больше предыдущего, то элемент не является первым для цепочки. Отсюда получаем следующее элегантное решение: Код (C): #include <stdio.h> #define RANK 4 #define ITEMS (1 << RANK) #define TOTAL (ITEMS << 1) int main() { int buf[TOTAL]; int tmp; for (size_t i=0; i<TOTAL; ++i) buf[i] = i; for (size_t i=1; i<ITEMS; ++i) { // Check that previous element in chain is not less than current size_t j = (i & 1) ? (i >> 1) | ITEMS : (i >> 1); if (j < i) continue; size_t k = i; while (1) { k = (k & ITEMS) ? ((k << 1) & (TOTAL - 1)) | 1 : (k << 1); if (k == i) break; tmp = buf[i]; buf[i] = buf[k]; buf[k] = tmp; } } printf("Result:"); for (size_t i=0; i<TOTAL; ++i) printf(" %02X", buf[i]); printf("\n"); }
Таким алгоритмом цепочка 5 10 20 9 18 будет переставлена два раза, там точно правильный ответ получается?!
Мы проверяем, является ли перебираемый индекс (от 0 до длина массива/2 - 1) первым (самым младшим) элементом последовательности. До i=20 цикл просто не дойдёт, так как 20 > 16. Добавил отладочный вывод в код: Код (Text): Shuffle i=1 00 10 01 03 02 05 06 07 04 09 0A 0B 0C 0D 0E 0F 08 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F Shuffle i=3 00 10 01 11 02 05 03 07 04 09 0A 0B 06 0D 0E 0F 08 18 12 13 14 15 16 17 0C 19 1A 1B 1C 1D 1E 1F Shuffle i=5 00 10 01 11 02 12 03 07 04 14 05 0B 06 0D 0E 0F 08 18 09 13 0A 15 16 17 0C 19 1A 1B 1C 1D 1E 1F Shuffle i=7 00 10 01 11 02 12 03 13 04 14 05 0B 06 0D 07 0F 08 18 09 19 0A 15 16 17 0C 1C 1A 1B 0E 1D 1E 1F Shuffle i=9 00 10 01 11 02 09 03 13 04 0A 12 0B 06 0D 07 0F 08 18 14 19 05 15 16 17 0C 1C 1A 1B 0E 1D 1E 1F Shuffle i=11 00 10 01 11 02 09 03 13 04 0A 12 15 06 16 07 0F 08 18 14 19 05 1A 0B 17 0C 1C 0D 1B 0E 1D 1E 1F Shuffle i=13 00 10 01 11 02 09 03 13 04 0A 12 1A 06 0B 07 0F 08 18 14 19 05 0D 15 17 0C 1C 16 1B 0E 1D 1E 1F Shuffle i=15 00 10 01 11 02 09 03 13 04 0A 12 1A 06 0B 07 17 08 18 14 19 05 0D 15 1B 0C 1C 16 1D 0E 1E 0F 1F Result: 00 10 01 11 02 09 03 13 04 0A 12 1A 06 0B 07 17 08 18 14 19 05 0D 15 1B 0C 1C 16 1D 0E 1E 0F 1F
при 32х элементах есть 6 цепочек по 5 элементов, а код переставляет 8 цепочек, 2 из них переставляются по 2 раза, и 10 элементов оказываются не там где должны: Result: 00 10 01 11 02 09 03 13 04 0A 12 1A 06 0B 07 17 08 18 14 19 05 0D 15 1B 0C 1C 16 1D 0E 1E 0F 1F