LINUX.ORG.RU

Яблоконь LOR Code Wars Championship: раскрытие фигурных скобок как в баше

 ,


0

5

Я вижу тут всем нравятся подобные темы в стиле @kompospec.

Задача: реализовать элегентное раскрытие фигурных скобок как в баше:

Условия:

  • есть некая функция expand, приниающая строку, раскрывающая фигурные скобки с вариантами, перечисленными через запятую, и возвращающая массив с результатами. Пример реализации:
def combine(a: list, b: list) -> list:
    rv = []
    for x in a:
        for y in b:
            rv.append(x + y)
    return rv


def append(lst: list[str], add: str) -> list[str]:
    return [x + add for x in lst]


def expand(s: str) -> list[str]:
    head, *parts = s.split("{")
    rv = [head]
    for part in parts:
        options, rest = part.split("}", 1)
        rv = append(combine(rv, options.split(",")), rest)
    return rv


assert expand("~/{.local/share,.config,.cache}/{foo,bar}-package") == ['~/.local/share/foo-package', '~/.local/share/bar-package', '~/.config/foo-package', '~/.config/bar-package', '~/.cache/foo-package', '~/.cache/bar-package']
  • Язык реализации может быть любой.
  • Желательно чтобы функция могла обрабатывать вложенные скобки. Можно так же добавить поддержку срезов типа {a..z} и предусмотреть экранирование символово {},.
  • Предпочтительнее декларативный стиль.

Дополнения:

Что за вложенные скобки? - А вот они:

~   
❯ echo /path/to/{foo,ba{r,z}}.txt
/path/to/foo.txt /path/to/bar.txt /path/to/baz.txt

Примеры решений

Рекурсивный вызов:

# Модифицированная версия отсюда https://algo.monster/liteproblems/1096
def parse_range(s: str) -> list[str | int]:
    range_parts = s.split("..", 1)
    is_num_range = all(x.isdigit() for x in range_parts)
    first, last = map((ord, int)[is_num_range], range_parts)
    # first, last = min(first, last), max(first, last)
    res = range(first, last + 1)
    return list(res if is_num_range else map(chr, res))


def expand(s: str) -> set[str]:
    rv = set()

    def dfs(exp: str) -> None:
        try:
            inner, after = exp.split("}", 1)
        except ValueError:
            rv.add(exp)
            return

        before, inner = inner.rsplit("{", 1)

        for item in parse_range(inner) if ".." in inner else inner.split(","):
            dfs(f"{before}{item}{after}")

    dfs(s)

    return rv

Недостаток: без set будут дублироваться элементы.

Функциональный стиль:

def expand str
  _, res, _ = str.each_char.reduce([[''], [''], []]) do |(pre, cur, acc), c|
    case c
    when '}'; [acc + cur, acc + cur, []]
    when '{'; [cur, cur, acc]
    when ','; [pre, pre, acc + cur]
    else    ; [pre, cur.map { |x| x + c }, acc]
    end
  end
  res
end

Недостаток: неправильно обрабатывает запятую на первом уровне вложенности.

★★

Последнее исправление: rtxtxtrx (всего исправлений: 5)

Ответ на: комментарий от MKuznetsov

Так?

lcs←{
     ⎕IO←0
     better←{⊃(</+/¨⍺ ⍵)⌽⍺ ⍵}
     cmb←{↑,⊃∘.,/(⊂⊂⍬),⍵}
     rr←{∧/↑>/1 ¯1↓[1]¨⊂⍵}
     hmr←{∨/(rr ⍵)∧∧/⍵=⌈\⍵}
     rnb←{{⍵/⍳⍴⍵}¨↓[0]×⍵}
     good←hmr∘cmb∘rnb
     a w←(</⊃∘⍴¨⍺ ⍵)⌽⍺ ⍵ 
     matches←a∘.=w
     aps←{⍵[;⍒+⌿⍵]}∘{(⍵/2)⊤⍳2*⍵} 
     swp←{⍵/⍨∧⌿~(~∨⌿⍺)⌿⍵}   
     ss←matches swp aps⊃⍴w  
     w/⍨{
         ⍺←0⍴⍨⊃⍴⍵              
         (+/⍺)≥+/⍵[;0]:⍺        
         this←⍺ better{⍵×good ⍵/matches}⍵[;0]  
         1=1⊃⍴⍵:this                      
         this ∇ 1↓[1]⍵    
     }ss
 }
monk ★★★★★
()
Ответ на: комментарий от monk

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

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)

Херня какая-то получилась. Всё чаще за собой обнаруживаю что я теряюсь когда дело заходит о комбинациях чего либо.
Кабзда… Деменция и разжижение мозжечка не иначе

local function expand(inp)
    local out = { }
    local box = { }
    local pre = box
    for x in inp:gmatch('{(.-)}') do
       box.block = x
       box.items = { }
       box.next_box = { }
       for y in (x..','):gmatch('(.-),') do
           box.items[#box.items + 1] = y
       end
       box = box.next_box
    end
    local function substritute(inp,box,out)
       if box and box.items then
          for i,_ in ipairs(box.items) do
              local item = inp:gsub(box.block,box.items[i])
              if not item:find(',') then
                 out[#out+1] = item
              end
              substritute(item,box.next_box,out);
          end
       end
    end
    substritute(inp,pre,out)
    for k,v in ipairs(out) do
        out[k] = v:gsub('{',''):gsub('}','')
    end
    print(table.concat(out,'\n'))
end

expand("~/{.local/share,.config,.cache}/{foo,bar}-package")

Работает строго с единичными {вариантами,для,подмены}, без выкрутасов и фичей что в шапке описаны.

dron@gnu:~/Рабочий-стол$ lua test.lua 
~/.local/share/foo-package
~/.local/share/bar-package
~/.config/foo-package
~/.config/bar-package
~/.cache/foo-package
~/.cache/bar-package
dron@gnu:~/Рабочий-стол$ 

Но даже такая штука мне самому пригодится, закину тоже в закладки профиля. Только при случае надо переписать.

Каковое моё место в таблице лидеров чемпионата? Первый!! Снизу? :D
И каков мой приз?!!11 И где все участники?…

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 4)
Ответ на: комментарий от LINUX-ORG-RU

я еще накину:

In [14]: def parse_range(s: str) -> list[str]:
    ...:     start, end = s.split("..")
    ...:     return list(
    ...:         map(str, range(int(start), int(end) + 1))
    ...:         if start.isdigit()
    ...:         else map(chr, range(ord(start), ord(end) + 1))
    ...:     )
    ...: 

In [15]: parse_range('A..D')
Out[15]: ['A', 'B', 'C', 'D']

Это для парсинга {1..10} или {a..z}

rtxtxtrx ★★
() автор топика
Последнее исправление: rtxtxtrx (всего исправлений: 1)
Ответ на: комментарий от rtxtxtrx
local function range(str)
    local out = { }
    local qot = "'"
    for x,y in str:gmatch('(%w+)%.%.(%w+)') do
        local p = tonumber(x) and tonumber(y) and 1 or 0
        local a = tonumber(x) or x:byte()
        local b = tonumber(y) or y:byte()
        local s,e,n = a,b,1
        if a > b then a=b e=a n=-1 end
        for i=s,e,n do
            if p == 0 then
               out[#out+1]= qot..string.char(i)..qot
            else
               out[#out+1]= qot..i..qot
            end
        end
     end
     print(table.concat(out,','))
end
range("1..15")
range("A..P")
range("P..A")
range("15..1")
dron@gnu:~/Рабочий-стол$ lua x.lua 
'1','2','3','4','5','6','7','8','9','10','11','12','13','14','15'
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P'
'P','O','N','M','L','K','J','I','H','G','F','E','D','C','B','A'
'15','14','13','12','11','10','9','8','7','6','5','4','3','2','1'

Тоже мне пригодится, тоже в профиле сохраню :D

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)
#!1

разбор шаблон =
  длина = длина-строки шаблон
  проход позиция результат в-скобках =
    если
      (позиция == длина)
        значения результат позиция
      (в-скобках && (шаблон[позиция] == #\} || шаблон[позиция] == #\,))
        если пустой? $ оставшиеся результат тогда
          значения диапазон(результат[0]) позиция
          иначе
          значения результат позиция
      (шаблон[позиция] == #\{)
        значения после-группы в-группе = прочитать-группу (позиция + 1)
        проход после-группы слить(результат в-группе) в-скобках
      (позиция < длина - 1 && шаблон[позиция] == #\\)
        проход
          позиция + 2
          слить результат
            список $ строка шаблон[позиция + 1]
          в-скобках
      иначе
        проход
          позиция + 1
          слить результат
            список $ строка шаблон[позиция]
          в-скобках

  прочитать-группу начало слова(пустой-список) =
    значения слово остаток = проход начало '("") истина
    если
      (остаток == длина)
        значения начало '("{")
      (шаблон[остаток] == #\})
        значения
          остаток + 1
          слова ++ слово
      (шаблон[остаток] == #\,)
        прочитать-группу
          остаток + 1           
          слова ++ слово           
      иначе
        значения начало '("{") 
        
  значения ответ _ = проход 0 '("") ложь
  ответ

слить список . ещё-списки =
  если пустой? ещё-списки тогда
    список
    иначе
    применить слить
      применить добавить
        отобразить
          функция (строка1)
            отобразить
              функция (строка2) $ строка1 ++ строка2
              первый ещё-списки
          список
      оставшиеся ещё-списки

диапазон слово =
  длина = длина-строки слово
  конец = длина - 2
  пусть цикл (позиция(1))
    если
      (позиция >= конец)
        список слово
      (слово[позиция] == #\. && слово[позиция + 1] == #\.)
        если длина == 4 тогда
          диапазон-символов
            литера->число слово[0]
            литера->число слово[3]
          иначе
          диапазон-чисел
            подстрока слово 0 позиция
            подстрока слово $ позиция + 2
      иначе
        цикл $ позиция + 1
        
диапазон-символов начало конец =
  элемент число =
    строка $ число->литера число
  диапазон-элементов элемент начало конец

диапазон-чисел начало конец =
  начальное-число = строка->число начало
  конечное-число = строка->число конец
  если точное-целое? начальное-число && точное-целое? конечное-число тогда
    диапазон-элементов число->строка начальное-число конечное-число
    иначе
    начало ++ ".." ++ конец

диапазон-элементов элемент начало конец =
  если
    (начало > конец)
      элемент начало : диапазон-элементов элемент (начало - 1) конец
    (начало < конец)
      элемент начало : диапазон-элементов элемент (начало + 1) конец
    иначе
      список $ элемент начало
> разбор "/path/to/{foo,ba{r..z}}.txt"
  
'("/path/to/foo.txt"
  "/path/to/bar.txt"
  "/path/to/bas.txt"
  "/path/to/bat.txt"
  "/path/to/bau.txt"
  "/path/to/bav.txt"
  "/path/to/baw.txt"
  "/path/to/bax.txt"
  "/path/to/bay.txt"
  "/path/to/baz.txt")

> разбор "/path/to/{foo,ba{r,z}}.txt"
  
'("/path/to/foo.txt" "/path/to/bar.txt" "/path/to/baz.txt")

> разбор "~/{.local/share,.config,.cache}/{foo,bar}-package"
  
'("~/.local/share/foo-package" "~/.local/share/bar-package" "~/.config/foo-package" "~/.config/bar-package" "~/.cache/foo-package" "~/.cache/bar-package")

> разбор "foo-{15..1}"
  
'("foo-15" "foo-14" "foo-13" "foo-12" "foo-11" "foo-10" "foo-9" "foo-8" "foo-7" "foo-6" "foo-5" "foo-4" "foo-3" "foo-2" "foo-1")
monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 4)

~/{.local/share,.config,.cache}/{foo,bar}-package

можно непосредственно преобразовать в код. Например скобки заменить на вызов корутин somelang.

или развернуть все рекурсии в один цикл: добавить псевдо символ N> . На старте все N> ставятся после { , перебор завершается когда все N> указывают на } ; само N указывает на приоритет перебора

Странно что на ЛОР такие подходы не прозвучали.

Мельчают люди, мельчают :-(

MKuznetsov ★★★★★
()
Ответ на: комментарий от MKuznetsov

Одно из самых коротких решений вот:


def expand str
  _, res, _ = str.each_char.reduce([[''], [''], []]) do |(pre, cur, acc), c|
    case c
    when '}'; [acc + cur, acc + cur, []]
    when '{'; [cur, cur, acc]
    when ','; [pre, pre, acc + cur]
    else    ; [pre, cur.map { |x| x + c }, acc]
    end
  end
  res
end
irb(main):016:0> expand("enable_{foo,ba{r,z}}biz")
=> ["enable_foobiz", "enable_barbiz", "enable_bazbiz"]

Оно однопроходное по строке (списку символов). Что там выше вбрасывали розету, там говно-решения. Точнее там один примера на питоне есть, его взяли на все языки переписали, но он корявенький

rtxtxtrx ★★
() автор топика
Последнее исправление: rtxtxtrx (всего исправлений: 1)
Ответ на: комментарий от monk

На JS выглядит как говно:

const expand = (s) => Array.prototype.reduce.call(s, ([pre, cur, acc], c) => {
  switch (c) {
    case '}':
      return [acc.concat(cur), acc.concat(cur), []]
    case '{':
        return [cur, cur, acc]
    case ',':
        return [pre, pre, acc.concat(cur)]
    default:
        return [pre, cur.map(v => v + c), acc]
  }
}, [[''], [''], []])[1]
rtxtxtrx ★★
() автор топика
Ответ на: комментарий от monk

Я знаю что он неправильно обрабатывает первый уровень, потому как там запятые не должны парситься, но функциональщина эта очень красива. Любой такой код заменяющий портянки 10 строками прекрасен

anonymous
()
Ответ на: комментарий от rtxtxtrx

Гулять, так гулять

a ← (⊂'') (⊂'') ''
br ← '{'
rrr ← {
	⍺ = '}': ((3⊃⍵), (2⊃⍵)) ((3⊃⍵), (2⊃⍵)) ''
	⍺ = br: (2⊃⍵) (2⊃⍵) (3⊃⍵)
	⍺ = ',': (⊃⍵) (⊃⍵) ((3⊃⍵), (2⊃⍵))
	(⊃⍵) ((2⊃⍵),¨⍺) (3⊃⍵)
}
expand ← {2⊃⊃rrr/(⌽⍵), ⊂a}
expand 'enable_{foo,ba{r,z}}biz'
┌─────────────┬─────────────┬─────────────┐
│enable_foobiz│enable_barbiz│enable_bazbiz│
└─────────────┴─────────────┴─────────────┘
monk ★★★★★
()
Ответ на: комментарий от monk

А вот так однострочник (делает то же самое)

expand ← {2⊃⊃{⊃((⍺='{},'),~⍺∊'{},')/((2⊃⍵) (2⊃⍵) (3⊃⍵)) (((3⊃⍵), (2⊃⍵)) ((3⊃⍵), (2⊃⍵)) '') ((⊃⍵) (⊃⍵) ((3⊃⍵), (2⊃⍵))) ((⊃⍵) ((2⊃⍵),¨⍺) (3⊃⍵))}/(⌽⍵), ⊂(⊂'')(⊂'')''}
monk ★★★★★
()
Ответ на: комментарий от monk

Если бы это сейчас нашли на стене пещеры подумали бы что это очередная картинкопись как у древних египтян и тут прослеживается фаллическая символика и какой то ритуал про плодородие, ну реально 3⊃⍵ по Фрейду , а это обнимашки ⊂(⊂''), а это уже откровенная порнография 2⊃⍵, а это вообще незаконно 2⊃⊃{⊃((⍺='{},' :D

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Решение из книжки с алгоритмами такое (и оно тож, ес-но, кривое):

def expand(s: str) -> set[str]:
    rv = set()

    def dfs(exp: str) -> None:
        closing_pos = exp.find("}")

        if closing_pos == -1:
            rv.add(exp)
            return

        opening_pos = exp.rfind("{", 0, closing_pos)

        before = exp[:opening_pos]
        after = exp[closing_pos + 1 :]

        for inside in exp[opening_pos + 1 : closing_pos].split(","):
            dfs(before + inside + after)

    dfs(s)

    return rv

Но я не осилил сделать по красоте с одним проходом. Ну собс-но задача довольно таки сложная. И ее спрашивали на собеседовании в FAANG раньше.

Я же использую эту функцию для генерации списка путей для сканера:

[
    {
        "name": "site backup",
        "path": "/{site,www,backup}.{zip,tar.{g,x}z}",
        "condition": "status_code == 200 && mime_type != 'text/html'",
    },
]

In [15]: expand("/{site,www,backup}.{zip,tar.{g,x}z}")
Out[15]: 
{'/backup.tar.gz',
 '/backup.tar.xz',
 '/backup.zip',
 '/site.tar.gz',
 '/site.tar.xz',
 '/site.zip',
 '/www.tar.gz',
 '/www.tar.xz',
 '/www.zip'}
rtxtxtrx ★★
() автор топика
Последнее исправление: rtxtxtrx (всего исправлений: 6)
Ответ на: комментарий от rtxtxtrx

И ее спрашивали на собеседовании в FAANG раньше.

Значит я молодец :) Но не вижу на почте офферы от большегрудых рекрутотян :D

Я же использую эту функцию для генерации списка путей для сканера:

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

Чемпионат наверное закончен, объявляй победителем @monk и до следующей темы чей-то, «смари как могу»

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 3)

Ну, яблоконь-не яблоконь, а яблоконёк-горбунок вышел. Изначально была простая казалось бы идея. В один проход пройтись по строке, сформировать форматную строку и список элементов для перестановок, сделать все возможные перестановки, а потом просто слить изменения штатной функцией форматирования. Т.е. на входе "str{1,2}", в промежутке ("str~a" '("1" "2")), которые затем превращаются в вызов (format nil "str~a" "1"). Однако выяснилось, что format тормозит всю программу и в попытках ускорить родилось это:

(ql:quickload "alexandria")
(ql:quickload :split-sequence)
(ql:quickload "str")

(defun parse-loop (string)
  "Конечный автомат, возвращает форматный список и элементы для комбинации"
  (let ((template '())
	(options '())
	(current-option '())
	(current-options '())
	(flag-opt nil))
    (loop for c across string do
      (cond
	((char= c #\{) 			;новые опции
	 (progn
	   (setf flag-opt t)
	   (setf current-options '()) ;очищаем список новых опций
	   (push #\Null template)))   ;место для подстановки
	((char= c #\})			;опции закончились
	 (progn
	   (setf flag-opt nil)
	   (push current-option current-options)
	   (setf current-option '())
	   (push current-options options))) ;список новых опций отправляем к старым
	(flag-opt
	 (if (char= c #\,)		;разделитель опций
	     (progn
	       (push current-option current-options)
	       (setf current-option '())
	       )
	     (push c current-option)
	     ))
	(t (push c template))))
    (list template options)))

(defun get-template-list (data)
  (reverse
   (mapcar #'(lambda (seq) (coerce (reverse seq) 'string))
     (split-sequence:split-sequence #\Null (car data)))))

(defun get-opts (data)
  (reverse
   (mapcar
    #'(lambda (lst1)
	(reverse
	 (mapcar
	  #'(lambda (lst2) (coerce (reverse lst2) 'string))
	  lst1)))
    (cadr data))))

(defun perm1 (xs accumulator)
  (mapcar #'(lambda (x) (cons x accumulator)) xs))

(defun permutate-step (xs ys)
  (mapcan #'(lambda (y)
	      (perm1 xs y))
	  ys))

(defun permutate (options)
  (mapcar #'alexandria:flatten
	  (reduce #'permutate-step options)))

(defun fill-string (template-list options)
  (apply #'str:concat
	 (mapcar #'str:concat template-list options)))

(defun expand-string (template-list list-of-options)
  (mapcar #'(lambda (options)
	      (fill-string template-list options))
	  list-of-options))

(defun expand (string)
  (let* ((data (parse-loop string))
	 (template-list (get-template-list data))
	 (list-of-options (permutate (get-opts data))))
    (expand-string template-list list-of-options)))

По скорости

* (time (dotimes (i (expt 10 6)) (expand "~/{.local/share,.config,.cache}/{foo,bar}-package")))
Evaluation took:
  1.923 seconds of real time
  1.755259 seconds of total run time (1.745410 user, 0.009849 system)
  [ Real times consist of 0.112 seconds GC time, and 1.811 seconds non-GC time. ]
  [ Run times consist of 0.106 seconds GC time, and 1.650 seconds non-GC time. ]
  91.26% CPU
  4,984,881,686 processor cycles
  4,271,744,352 bytes consed

Тогда как питонопрога из начального поста

>>> def benchmark(n):
...     start_time = time.time()
...     for i in range(n):
...         expand(example)
...     end_time = time.time()
...     print("--- {} seconds ---".format(end_time - start_time))
...
>>> benchmark(10**6)
--- 2.9146955013275146 seconds ---
>>> benchmark(10**6)
--- 3.0261192321777344 seconds ---

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

ugoday ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Подозреваю, это как с регулярными выражениями. Если постоянно с ними сталкиваешься и вошёл в транс, можно не только писать, но и читать. А если нет, то проще сразу застрелиться.

ugoday ★★★★★
()

Совсем не яблоконь, получился немного сложный и немного объёмный код. Идея в том, чтобы оперировать сугубо индексами начала и размерами отрезков, не производя операций над строками. Таким образом достигается чуть большая производительность.

Span - структура, хранящая в себе индекс начала подстроки и её длину.

class Span {
 public:
  Span (std::size_t start, std::size_t length)
      : start (start),
        length (length)
  {}
  std::size_t start;
  std::size_t length;
};

SpanString - массив Span, которые если по очереди раскрыть, представляют из себя вариант раскрытия строки.

typedef std::vector<Span> SpanString;

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

  1. Текст, содержащий скобки, подлежащие раскрытию
  2. Cкобка, содержащая тексты, подлежащие раскрытию.
class View {
 public:
  
  View (std::size_t start, std::size_t length)
      : on (Span (start, length)),
        subviews ({})
  {
  }

  ~View ()
  {
    for (auto &replacement: this->subviews)
      delete replacement;
  }

  Span on;
  std::vector<View *> subviews;
};

Например, для строки /path/to/{foo,ba{r,z}}.txt:

start: 0; end: 25; 
  start: 9; end: 21; 
    start: 10; end: 12; 
    start: 14; end: 20; 
      start: 16; end: 20; 
        start: 17; end: 17; 
        start: 19; end: 19; 

Далее простеньким алгоритмом проходимся по строке и собираем из неё View, попутно проверяя баланс скобок и экранирования:

std::expected<View *, ExpansionError> build_view (const std::string &line)
{
  std::stack<View *> balance;
  balance.push (new View (0, line.length ()));

  for (size_t i = 0; i < line.size (); i++)
    {
      char current = line[i];

      bool is_root = balance.size () == 1;
      bool is_escaped = i > 0 && line[i - 1] == '\\';

      Token token = plain;
      if (current == '{' && !is_escaped)
        token = Token::expansion_open;
      else if (current == '}' && !is_escaped)
        token = Token::expansion_close;
      else if (current == ',' && !is_escaped && !is_root)
        token = Token::expansion_sep;

      switch (token)
        {
          case Token::expansion_open:
            {
              View *top_view = balance.top ();

              View *new_subview = new View (i, 0);
              View *new_subview_1st_option = new View (i + 1, 0);
              new_subview->subviews.push_back (new_subview_1st_option);

              if (balance.size () == 1)
                // blah blah {hello, world} test {
                // top subview is closed (root is the only case)
                // just add new replacement
                top_view->subviews.push_back (new_subview);
              else
                // blah blah {hello, world{
                // last subview is opened
                // add new replacement for it's last replacement option
                top_view->subviews.back ()->subviews.push_back (new_subview);

              balance.push (new_subview);
              break;
            }

          case Token::expansion_sep:
            {
              View *top_view = balance.top ();

              View *last_replacement = top_view->subviews.back ();
              std::size_t previous_replacement_start = last_replacement->on.start;
              std::size_t previous_replacement_length = i - previous_replacement_start;
              last_replacement->on.length = previous_replacement_length;

              View *new_subview = new View (i + 1, 0);
              top_view->subviews.push_back (new_subview);

              break;
            }

          case Token::expansion_close:
            {
              View *top_view = balance.top ();
              std::size_t view_start = top_view->on.start;
              std::size_t view_length = i - view_start + 1;
              top_view->on.length = view_length;

              View *last_replacement = top_view->subviews.back ();
              std::size_t last_replacement_start = last_replacement->on.start;
              std::size_t last_replacement_length = i - last_replacement_start;
              last_replacement->on.length = last_replacement_length;

              balance.pop ();
              break;
            }

          default:
            break;
        }
    }

  if (balance.size () != 1)
    {
      return std::unexpected (incorrect_expression);
    }

  return balance.top ();
}

Дальше применяются два немного разных алгоритма рекурсивно:

  1. expand_text - получает все варианты раскрытия строки со скобками. При помощи expand_brackets получает все варианты раскрытия для каждой представления каждой скобки. Потом аккуратно перебирает их комбинации и ещё более аккуратно составляет из них SpanStringи. Тривиальный случай - строка текста без скобок.
  2. expand_brackets - получает все варианты раскрытия скобки со строками. При помощи expand_string для каждой вложенной перечисленной через запятую строки узнаёт все её комбинации раскрытия. Их он не перебирает, а только складывает в один общий массив.
std::vector<SpanString> expand_brackets (const View *view);
std::vector<SpanString> expand_text (const View *view);

std::vector<SpanString> expand_brackets (const View *view)
{
  std::vector<SpanString> replacements;
  replacements.reserve (view->subviews.size ());

  // Collect all replacements of all options within brackets
  for (auto &replacing_subview: view->subviews)
    {
      std::vector<SpanString> replacements_per_subview = expand_text (replacing_subview);
      for (SpanString &replacement: replacements_per_subview)
        replacements.push_back (replacement);
    }

  return replacements;
}

std::vector<SpanString> expand_text (const View *view)
{
  bool is_trivial = view->subviews.empty ();
  if (is_trivial)
    return {{view->on}};

  // All brackets all replacement options
  std::vector<std::vector<SpanString>> brackets_replacements;
  brackets_replacements.reserve (view->subviews.size ());

  for (auto &replacement: view->subviews)
    brackets_replacements.push_back (expand_brackets (replacement));

  // All expansions of the string
  std::vector<SpanString> expansions;

  // Look for all replacements combinations
  std::vector<size_t> mask (brackets_replacements.size () + 1, 0);
  while (mask.back () == 0)
    {
      SpanString expanded = {{Span (view->on.start, 0)}};

      for (
          std::size_t current_replacement_idx = 0;
          current_replacement_idx < view->subviews.size ();
          current_replacement_idx++)
        {
          std::size_t replacement_start = view->subviews.at (current_replacement_idx)->on.start;
          std::size_t replacement_length = view->subviews.at (current_replacement_idx)->on.length;
          std::size_t replacement_end = replacement_start + replacement_length;

          // Fix previous text block length
          expanded.back ().length = replacement_start - expanded.back ().start;

          // Push all spans of the replacement
          for (Span &span: brackets_replacements
              .at (current_replacement_idx)
              .at (mask.at (current_replacement_idx)))
            expanded.push_back (span);

          expanded.emplace_back (replacement_end, 0);
        }

      // Fix last text block length
      expanded.back ().length = view->on.start + view->on.length - expanded.back ().start;
      if (expanded.back ().length == 0)
        expanded.pop_back ();

      expansions.push_back (expanded);

      // Go to next combination
      mask.at (0)++;
      for (int i = 0; i < brackets_replacements.size () && mask.at (i) == brackets_replacements.at (i).size ();)
        mask.at (i) = 0, mask.at (i + 1)++, i++;
    }

  return expansions;
}

Естественно, потом нужно обратно преобразовать (при необходимости) SpanString в нормальные строки.

Для приведённого выше теста у меня работает за 1587ms

witaway
()
Ответ на: комментарий от witaway

Код можно значительно улучшить (считайте, это MVP алгоритма), но я уже очень сильно устал:

  1. Устранить кое-какие протечки памяти.
  2. Хранить спаны и вектора спанстрингов не на стеке, а в куче. Тогда, уверен, можно значительно ускорить работу засчёт избежания копирования.
  3. Сэкономить время на выделение памяти использовав арена-аллокатор.
  4. Всё собрать в какой-нибудь удобный класс типа StringExpander
  5. Превратить SpanString в какой-нибудь удобный класс. Чтобы собирать из них строки не вручную. К тому же, можно придумать оператор обращения по индексу использовав бинарный поиск.
  6. Можно использовать std::span и ничего не поломается.
  7. Да и вообще всё это безобразие надо привести в нормальный вид, подшлифовать, что ли…
witaway
()
Последнее исправление: witaway (всего исправлений: 2)
Ответ на: комментарий от witaway

Для приведённого выше теста у меня работает за 1587ms

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

rtxtxtrx ★★
() автор топика
Ответ на: комментарий от rtxtxtrx

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

Можно было, почти уверен, сделать раза в 1.5 оптимальнее, если сделать приведённые выше исправления. Я пока пытался реализовать алгоритм и починить все баги, работу с памятью сделал лишь бы как. Ещё, думаю, при очень большом желании можно было бы избавиться от рекурсии.

Зато было интересно писать.

witaway
()
Последнее исправление: witaway (всего исправлений: 3)
Ответ на: комментарий от ugoday

Решил переписать, чтоб не так страшно было, вместо одного большого loop’а — две функции, одна разбирает стабильную строчку, пока не встретит «{» и переключается на другую, которая разбирает — варианты, пока не встретит «}». Скорость в принципе та же, поскольку алгоритм не поменялся: отдельно разбираем строку, отдельно делаем все перестановки, потом объединяем.

(ql:quickload "alexandria")
(ql:quickload "str")

(defun is-start-options-p (c) (char= c #\{))
(defun is-end-options-p (c) (char= c #\}))
(defun is-delimiter-options-p (c) (char= c #\,))

(defun add-to-acc (current xs)
  (let ((x (coerce (nreverse current) 'string)))
    (if (null xs) (setf xs (list x))
	(push x (cdr (last xs))))
    xs))

;;prepare: current: (E e) group: (bB cC) options: NIL
(defun prepare-options (current-option current-group options)
  (let* ((current (list (coerce (nreverse current-option) 'string)))
	 (group (list (append current-group current))))
    (append options group )))


(defun switch-to-parse-options (rest current template options)
  "Мы нашли {, сохраняем текущую строку к найденным шаблонам и начинаем разбирать опции."
  (parse-options rest (add-to-acc current template) nil nil options))

(defun parse-template (data current template options)
  (if (null data) (list (add-to-acc current template) options)
      (let ((c (car data)))
	(if (is-start-options-p c)
	    (switch-to-parse-options (cdr data) current template options)
	    (parse-template (cdr data) (cons c current) template options)))))

(defun parse-options (data template current-option current-group options)
    (if (null data) (list template (prepare-options current-option current-group options))
	(let ((c (car data)))
	  (cond
	    ((is-end-options-p c)
	     (parse-template (cdr data) nil template (prepare-options current-option current-group options)))
	    ((is-delimiter-options-p c)
	     (parse-options (cdr data) template nil (add-to-acc current-option current-group) options))
	    (t
	     (parse-options (cdr data) template (cons c current-option) current-group options))))))


(defun parse2 (string)
  (let ((data (coerce string 'list)))
    (if (is-start-options-p (car data))
	(parse-options (cdr data) nil nil '(()))
	(parse-template data nil nil '(())))))

(defun permutate-step (xs ys)
  (mapcan
   #'(lambda (y)
       (mapcar #'(lambda (x)
		   (cons x y))
	       xs))
   ys))

(defun permutate (options)
  (mapcar #'alexandria:flatten
	  (reduce #'permutate-step options)))

(defun fill-string (template-list options)
  (apply #'str:concat
	 (mapcar #'str:concat template-list options)))

(defun expand-string (template-list list-of-options)
  (mapcar #'(lambda (options)
	      (fill-string template-list options))
	  list-of-options))

(defun expand2 (string)
  (let* ((data (parse2 string))
	 (template-list (car data))
	 (options (remove-if #'null (cadr data)))
	 (list-of-options (permutate options)))
    (expand-string template-list list-of-options)))
ugoday ★★★★★
()