사용자 도구

사이트 도구


ps:problems:programmers:68938

문자열의 아름다움

ps
링크programmers.co.kr/…
출처프로그래머스
문제 번호68938
문제명문자열의 아름다움
레벨Level 5
분류

애드혹

시간복잡도O(n)
인풋사이즈n<=300,000
사용한 언어Python
해결날짜2021/01/25

풀이

  • 공식 블로그에 공식 해설이 이미 있기는 하다.
  • 그러나 해설에서는 세그먼트 트리를 이용해서 O(nlogn)에 푸는 방법을 설명하고 있지만, 수식을 잘 정리하면 O(n)에 푸는 것도 가능하다.
    • 사실 수식을 O(n)형태로 정리하는데 생각하는 시간이 상당히 오래 걸렸다. 실제로 대회같은 상황에서 이런 문제가 나오면 빠르게 세그먼트 트리 아이디어를 떠올려서 구현하는게 효과적일 것이고, 연습할때도 그런 방향으로 연습을 하는게 더 도움이 될 것같다.
  • 기본 아이디어는, 문자열 s의 아름다움은 s의 길이에서, s의 첫글자와 마지막 글자가 같으면 1을 빼고, s의 첫 두글자와 마지막 두글자가 같으면 1을 또 빼고, … 이런 방식으로 계산하는 것이다.
    • beauty(s) = len(s) - (1 if s[0] == s[-1] else 0) - (1 if s[:1] == s[-2:] else 0) - …
  • f[x]를 첫 x개 글자와 마지막 x개 글자가 같은 부분문자열의 갯수라고 하면, 모든 부분문자열 si의 아름다움의 합 sum(beauty(si) = sum(len(si)) - f[1] - f[2] - …. 이 된다.
  • $\sum_i(len(s_i)) = \sum_{i=1}^n{\sum_{j=i+1}^n(j - i)} = \frac{n(n-1)(n+1)}{6} $ 은 쉽게 유도 가능
  • f[x] = fa[x]+fb[x]+.. 이런식으로 문자별로 쪼개서 생각해보자. fa[x]는 첫 x개 글자와 마지막 x개 글자가 모두 'a'인 문자열의 수이다.
  • 문자열에 들어있는 'a'중에서 두 개를 뽑으면, 그 두개를 양 끝으로 하는 부분문자열은 a로 시작하고 a로 끝난다. 그러니까 fa[1]은 문자열에 들어있는 모든 'a'중 에서 두개를 뽑는 경우의 수와 같다. 문자열에 들어있는 모든 'a'의 개수를 ca[1] 이라고 하면 fa[1] = C(ca[1], 2) = ca[1] * (ca[1] - 1) / 2 이다
  • 또 문자열에 들어있는 'aa'중에서 두 개를 뽑으면, 그 두개를 양 끝으로 하는 부분문자열은 aa로 시작하고 aa로 끝난다. 그러니까 fa[2]는 비슷하게 fa[2] = C(ca[2], 2) = ca[2] * (ca[2] - 1) / 2 가 된다.
  • 이제 식은 간단해졌다. ca값만 다 구해주면, fa를 구할수 있다.
  • 전체 문자열을 앞에서부터 스캔하면서 ca를 바로 업데이트하는 것은 비효율적이다. 문자열을 'aaa'까지 읽어서 ca[1]=3, ca[2]=2, ca[3]=1 을 구했는데, 다음 읽은 글자가 또 'a'면, ca[1],ca[2],ca[3],ca[4]를 모두 1씩 증가시켜야 한다. 최악에는 O(n^2)이 걸린다.
  • 대신 문자열에 있는 연속된 'a'덩어리가 몇개씩 있는지를 저장한다. 'aaa_aa_aa_a_a_a_a' 라면 count['a']={3:1, 2:2, 1:4} 가 된다. 이것은 O(n)에 가능하다.
  • 이제 저 count로부터, ca를 계산할 수 있다. ca[1] = 3*1 + 2*2 + 1*4, ca[2] = (3-1)*1 + (2-1)*2, ca[3] = (3-2)*1 이다.
  • 이것은 ca하나를 구하는 데에 O(n)처럼 보일수 있지만, 잘 조작하면 점화식 형태로 바꿀수 있어서, O(1)에 계산 가능하다.
    • t[i] = count[1]+count[2]+..count[i]
    • ca[i+1] = ca[i] - t[i]
    • t[i+1] = t[i] - count[i]
  • 이제 모든 ca[0:n_a], 그리고 모든 fa[0:n_a]를 O(n_a)에 구할 수 있다.
  • 그리고 모든 글자에 대해서 이걸 계산하는 것도. n_a + n_b + … < n 이므로 결국 O(n) 이다

코드

"""Solution code for "Programmers 68938. 문자열의 아름다움".

- Problem link: https://programmers.co.kr/learn/courses/30/lessons/68938
- Solution link: http://www.teferi.net/ps/problems/programmers/68938
"""

import collections
import itertools


def solution(s):
    # count_by_len_by_ch['x']: counts of 'x', 'xx', 'xxx', ...
    # ex) s: 'ababbab' => count_by_len_by_ch: {['a']: {1:3}, ['b']: {1:2, 2:1}}
    count_by_len_by_ch = collections.defaultdict(
        lambda: collections.defaultdict(int))
    for ch, group in itertools.groupby(s):
        count_by_len_by_ch[ch][len(list(group))] += 1

    # t[i] = counts[i] + counts[i + 1] + ... = t[i - 1] - counts[i - 1]
    # s[i] = i * counts[i] + (i + 1) * counts[i + 1] + ... = s[i - 1] - t[i - 1]
    # f(n) = sum((j - i) for i in range(n) for j in range(i + 1, n))
    #      = (n-1) * n * (n+1) // 6
    # answer = f(len(s)) - sum_ch( sum_l(C(s[l], 2))) )

    answer = (len(s) - 1) * len(s) * (len(s) + 1) // 6
    for count_by_len in count_by_len_by_ch.values():
        t = sum(count_by_len.values())
        s = sum(l * count for l, count in count_by_len.items())
        for l in range(1, max(count_by_len) + 1):
            answer -= s * (s - 1) // 2
            s -= t
            t -= count_by_len[l]
    return answer

토론

하민수, 2021/03/04 14:53
* 기본 아이디어는, 문자열 s의 아름다움은 s의 길이에서, s의 첫글자와 마지막 글자가 같으면 1을 빼고, s의 첫 두글자와 마지막 두글자가 같으면 1을 또 빼고, … 이런 방식으로 계산하는 것이다.



아름다움 수치는 s에서 서로다른 문자의 최대거리를 구하는건데 왜 (첫, 마지막) n개의 글자가 같은지를 비교하는건지 이해가안됩니다
하민수, 2021/03/05 04:54
좀더 읽어보고 이해했습니다
권현성, 2022/06/24 08:15
s -= t
t -= count_by_len[l]
왜 있는 건가요?
Teferi, 2022/06/24 16:33
풀이에서 이 부분을 옮겨놓은 코드입니다. 제가 보기에도 풀이를 읽기 어렵게 적어놓긴 했네요..
ca[i+1] = ca[i] - t[i]
t[i+1] = t[i] - count[i]
댓글을 입력하세요:
B L K M N
 
ps/problems/programmers/68938.txt · 마지막으로 수정됨: 2021/01/25 17:00 저자 teferi