не сможете даже определить, конечна ли вообще задаваемая ими группа.
Ну, я себе не ставлю такую задачу. Задача ставится так: даны групповые соотношения,
задающие конечную группу; написать программу, которая исходя из этих соотношений построит список элементов группы и таблицу умножения для них.
Затем, я эту задачу себе ещё упрощаю: я требую от исходных соотношений, чтобы в них не использовались обратные элементы, либо порядок образующей был понятен сразу из соотношений и все соответствующие обратные элементы можно было бы заменить положительной степенью. За счёт этого требования алфавит, из которого строятся строки элементов можно сократить сразу вдвое. Это не только улучшает быстродействие, но и существенно упрощает жизнь при написании программы (не надо, например, прописывать редукцию строк, когда подряд идут элемент и его обратный). С другой стороны, это требование отсеивать часть конечных групп, которые вполне можно было бы решить. Это плохо, но для начала и так сойдёт.
Последнее упрощение, которое я делаю, заключается в том, что я рассматриваю группы только с двумя образующими. Хотя, на самом деле, оно не на столько серьёзно, как предыдущее. Работа со строками не зависит от алфавита вообще; он понадобится только на этапе построения списка элементов. Это упрощение для того, чтобы получить хоть что-то работающее.
Формальная абстрактная процедура построения группы заключается в том, что из образующих и их обратных строятся
всевозможные комбинации, а затем, с помощью групповых соотношений, эти комбинации разбиваются на классы эквивалентности, которые и считаются элементами группы. Эта процедура на столько общая, что даже позволяет построить бесконечные группы. Разумеется, на практике она не применима. В связи с этим нужно разработать другой, более "приземлённый" алгоритм.
Я предлагаю делать следующее. У нас есть алфавит и единичный элемент (который есть в любой группе), представляемый пустой строкой. Мы дописываем к этому элементу сзади каждый из символов алфавита и добавляем их в очередь. Сам же единичный элемент добавляется в множество
G, которое по окончании процедуры должно стать группой. В этом заключается инициализация.
Итерация процедуры заключается в том, что мы достаём из очереди элемент
g (очередь типа FIFO — первый вошёл, первый вышел) и проверяем, присутствует ли этот элемент в заполняемом множестве
G. Если уже присутствует, то элемент отбрасывается. Если он отсутствует, то мы поступаем с ним так же, как с единичным элементом в инициализации: дописываем к
g по одному символу из алфавита, все результаты добавляем в очередь, а сам элемент
g добавляем во множество
G. Итерации продолжаются до тех пор, пока очередь не опустеет.
Продемонстрирую это на примере недавно изученной мной группы
. Алфавитом является множество символов
r, f.
0) Инициализация: множество
G содержит пустую строку
"", а очередь
Q — строки
"r", "f"1) Достаём из очереди элемент
"r", он в
G отсутствует, поэтому добавляем в очередь элементы
"rr", "rf", а элемент
"r" — в
G. Итого:
G содержит
"", "r", а очередь
Q —
"f", "rr", "rf"2) Достаём из
Q элемент
"f", он так же в
G отсутствует, добавляем его в
G, а в очередь — элементы
"fr", "ff". Итого:
G содержит
"", "r", "f", а очередь
Q —
"rr", "rf", "fr", "ff".
3) Достаём из
Q строку
"rr", она отсутствует в
G, добавляем её. В очередь
Q идут строки
"rrr", "rrf". Итого:
G содержит
"", "r", "f", "rr", а очередь
Q —
"rf", "fr", "ff", "rrr", "rrf".
Шаги 4), 5) проходят аналогично. По окончанию этих итераций множество
G будет содержать строки
"", "r", "f", "rr", "rf", "fr", а очередь
Q —
"ff", "rrr", "rrf", "rfr", "rff", "frr", "frf".
6) Из
Q достаётся строка
"ff". Это строка эквивалентна пустой строке
"", и поэтому она отбрасывается.
7) Из
Q достаётся строка
"rrr". Это строка так же эквивалентна пустой строке
"", и она тоже отбрасывается. В результате этих двух шагов множество
G не изменилось, а очередь
Q потеряло первые два элемента, не получив новых, и теперь содержит только строки
"rrf", "rfr", "rff", "frr", "frf".
Дальнейшие шаги будет удобно изобразить в виде выписки по одной итерации на строку выписки. В случае, когда очередная обрабатываемая строка оказывается эквивалентна какой-либо уже добавленной в множество
G, эта эквивалентность будет указана в виде равенства. Цифрами в круглых скобках помечены номера добавляемых в
G элементов (нумерация от нуля). Номера предыдущих добавленных совпадают с номером итерации.
Код:
8: "rrf" (6)
9: "rfr" (7)
10: "rff" = "r"
11: "frr" (8)
12: "frf" (9)
13: "rrfr" (10)
14: "rrff" = "rr"
15: "rfrr" (11)
16: "rfrf" = "frr"
17: "frrr" = "f"
18: "frrf" = "rfr"
19: "frfr" = "rrf"
20: "frff" = "fr"
21: "rrfrr" = "frf"
22: "rrfrf" = "rfrr"
23: "rfrrr" = "rf"
24: "rfrrf" = "rrfr"
В итоге получаем 12 элементов, как на картинке. Кроме всего прочего, этот алгоритм решает задачу о представлении каждого элемента в виде строки наименьшей длины.
Ключевым моментом в этом алгоритме, описанном словами, является фраза "
проверяем присутствие элемента во множестве G". Из выписки выше можно заметить, что для этого используются следующие
редуцирующие соотношения, являющиеся следствием групповых соотношений для группы
:
Код:
"rrr" -> ""
"ff" -> ""
"rfrf" -> "frr"
"frfr" -> "rrf"
"rrfrr" -> "frf"
"frrf" -> "rfr"
Каждое из этих преобразований укорачивает строку, поэтому я называю их "редуцирующими". Поскольку мы перебираем строки в порядке возрастания их длины, это будет означать, что укороченная строка уже имеется в множестве
G, а элементы представлены в наиболее компактном виде. То есть, если имеется полный набор редуцирующих соотношений, то построение списка элементов группы не составляет труда и легко алгоритмизируется.
Тут, правда, есть тонкий момент. Редуцирующее соотношение может оказаться не укорачивающим (не и не удлиняющим!), так как заменяемая и заменяющая строки будут иметь одинаковое количество символов. Над этим мне пришлось знатно поломать голову. Решение оказалось до смешного простым: такое соотношение надо использовать как и любое другое, но только лишь
в одну строну. Это можно объяснить следующим образом. Описанный выше алгоритм фактически является процедурой построения дерева элементов группы. Когда один элемент оказывается равным другому (в другом строковом представлении), соответствующие этим представлениям вершины дерева и вырастающие из них ветви сливаются воедино. Если эквивалентность связана с укорочением представляющей элемент строки, то он сливается с элементов на одном из предыдущих слоёв, более близких к корню дерева — единичному элементу
I. Поскольку из того элемента уже выросла ветвь (дерево строится слой за слоем), то этот элемент надо отбросить. Если же укорочения строки не происходит, то это означает, что сливающиеся элементы находятся в одном слое, а ветвь должна расти только из одного из них.
Следует ещё заметить, что если множество редуцирующих соотношений окажется не полным, то алгоритм будет генерировать строки всё возрастающей длины. Это будет означать, что либо группа является бесконечной, либо одно или несколько соотношений потеряно. Поведение программы в случае заведомо известных конечных групп — это пока единственный способ, позволяющий мне идентифицировать неполноту редуцирующих соотношений. Более прямого способа я не придумал. В связи с заявлениями выше, возможно его и нет. В любом случае, удобно обрывать работу программы при достижении какого-то порога по числу найденных элементов или по длине строки представления.
Это был рассмотрен второй этап решения задачи. Следующий, третий, этап — это
построение таблицы умножения. Тут всё просто. Есть список элементов, из которого мы берём все возможные пары. Строка-произведение является конкатенацией строк-сомножителей (порядок тоже важен) с последующей редукцией с помощью редуцирующих соотношений. Результат должен оказаться среди найденных элементов. Обратное, опять же, будет указывать на неполноту редуцирующих соотношений (в случае конечной группы). Затем таблицу можно проверить: убедиться, что все ячейки заполнены и что в строках нет повторяющихся элементов. Положительный результат проверки означает удачное построение группы.
Для примера, разобранного выше, результат будет иметь такой вид:
Код:
"" 0 1 2 3 4 5 6 7 8 9 10 11
"r" 1 3 4 0 6 7 2 10 11 8 5 9
"f" 2 5 0 8 9 1 7 6 3 4 11 10
"rr" 3 0 6 1 2 10 4 5 9 11 7 8
"rf" 4 7 1 11 8 3 10 2 0 6 9 5
"fr" 5 8 9 2 7 6 0 11 10 3 1 4
"rrf" 6 10 3 9 11 0 5 4 1 2 8 7
"rfr" 7 11 8 4 10 2 1 9 5 0 3 6
"frr" 8 2 7 5 0 11 9 1 4 10 6 3
"frf" 9 6 5 10 3 8 11 0 2 7 4 1
"rrfr" 10 9 11 6 5 4 3 8 7 1 0 2
"rfrr" 11 4 10 7 1 9 8 3 6 5 2 0
Correct table.
Пример программы на
Java, реализующий описанный выше алгоритм вот:
import java.util.*;
public class Study_Group_Generation_3 {
private static final char[] letters = {'r', 'f'};
private static final String[] targetStrings = {"rrr", "ff", "rfrf", "frfr", "frrf", "rrfrr"};
private static final String[] replacements = {"", "", "frr", "rrf", "rfr", "frf"};
// private static final char[] letters = {'s', 't'};
// private static final String[] targetStrings = {"sss", "tts", "stss", "sstt", "ssts", "ststt", "tstst", "tsttt", "stttt", "tttttt"};
// private static final String[] replacements = {"", "st", "tt", "tss", "tttt", "tsts", "ss", "s", "ts", "ststs"};
// private static final char[] letters = {'q', 'p'};
// private static final String[] targetStrings = {"qq", "ppqpp", "ppp", "pqpq", "qppqp", "qpqppq"};
// private static final String[] replacements = {"", "q", "qpq", "qpqp", "pqppq", "ppqp"};
// private static final String[] targetStrings = {"qq", "ppqpp", "ppp", "pqpq", "qppqp", "qpqppq"};
// private static final String[] replacements = {"", "q", "qpq", "qpqp", "pqppq", "ppqp"};
private static List<String> elementsNames = new ArrayList<>();
private static Queue<String> queue = new ArrayDeque<>();
private static int groupOrder;
private static int[][] multiplicationTable;
public static void main(String[] args) {
int k, l, m, maxLength;
String currentName, newName, whiteString;
boolean flag;
boolean[] checkFlags;
currentName = "";
k = 0;
maxLength = 0;
while (null != currentName && 100 > k) {
flag = true;
for (m = 0; targetStrings.length > m; ++m) {
if (currentName.endsWith(targetStrings[m])) {
flag = false;
break;
}
}
if (flag) {
System.out.println(String.format("%6d: \"%s\"", k, currentName));
elementsNames.add(currentName);
if (maxLength < currentName.length()) {
maxLength = currentName.length();
}
++k;
for (l = 0; letters.length > l; ++l) {
newName = currentName + letters[l];
queue.add(newName);
}
} else {
l = currentName.length() - targetStrings[m].length();
System.out.println(String.format(" - \"%s\" = \"%s%s\"",
currentName, currentName.substring(0, l), replacements[m]));
}
currentName = queue.poll();
}
System.out.println();
whiteString = new String(new char[maxLength]).replace('\0', ' ');
groupOrder = elementsNames.size();
multiplicationTable = new int[groupOrder][groupOrder];
for (k = 0; groupOrder > k; ++k) {
currentName = elementsNames.get(k);
System.out.print(String.format("\"%s\"%s",
currentName, whiteString.substring(currentName.length())));
for (l = 0; groupOrder > l; ++l) {
newName = currentName + elementsNames.get(l);
flag = true;
while (flag) {
flag = false;
for (m = 0; targetStrings.length > m; ++m) {
if (newName.contains(targetStrings[m])) {
newName = newName.replace(targetStrings[m], replacements[m]);
flag = true;
break;
}
}
}
flag = true;
for (m = 0; groupOrder > m; ++m) {
if (0 == elementsNames.get(m).compareTo(newName)) {
multiplicationTable[k][l] = m;
flag = false;
break;
}
}
if (flag) {
multiplicationTable[k][l] = -1;
}
System.out.print(String.format("%4d", multiplicationTable[k][l]));
}
System.out.println();
}
flag = true;
for (k = 0; groupOrder > k; ++k) {
checkFlags = new boolean[groupOrder];
for (l = 0; groupOrder > l; ++l) {
m = multiplicationTable[k][l];
if (-1 == m) {
flag = false;
} else {
if (checkFlags[m]) {
flag = false;
}
checkFlags[m] = true;
}
}
}
if (flag) {
System.out.println();
System.out.println("Correct table.");
}
}
}
Я погонял его для нескольких известных мне групп. Результаты работы удовлетворительные.