지금까지 작성한 question_list.html 이나 question_detail.html 등의 템플릿들은 HTML 표준을 따르는 구조가 아니었다.

그러나 웹 브라우저에 상관없이 같은 내용과 동작을 하도록 하려면 웹 표준을 지키는 HTML 문서를 작성할 필요가 있다.

이번에는 표준 HTML 구조에 대해 간단히 알아보고 템플릿 상속을 통해 그동안 작성한 템플릿들이 그 구조를 따르도록

해보기로 한다.

 

1. 표준 HTML 구조

{% load static %}
<!doctype html>
<html lang="ko">
<head>
    <!-- meta 태그 -->
    <meta charset="utf-8">
    <meta name="viewport" content="width-device-width", initial-scale=1, shrink-to-fit=no">
    
    <!-- css 파일 -->
    <link rel="stylesheet" type="text/css", href="{% static 'bootstrap.min.css' %}">
    <link rel="stylesheet" type="text/css", href="{% static 'style.css' %}">
    
    <!-- title 태그 -->
    <title>My Site Pybo</title>
</head>
<body>
	<!-- 본문 -->
</body>
</html>

표준 HTML 문서의 구조는 위와 같이 html, head, body 엘리먼트가 있어야한다. (여기서 엘리먼트란 태그로 둘러쌓인

블록을 의미한다.   예를 들어 <body> 는 body 태그이며 <body> ... </body> 는 body 엘리먼트가 된다.) 또한, 

head 엘리먼트에는 meta, title 등의 엘리먼트가 포함되어야 하며, css 파일은 head 엘리먼트 내에 있어야 한다.

 

2. 템플릿 상속

표준 HTML 문서의 구조를 알았으니 이제 그동안 작성해온 템플릿에 적용시켜야 한다. 그런데 HTML 표준을

적용하게되면 서로 다른 템플릿들 간에 body 엘리먼트 부분을 제외하고는 사실상 차이가 없어진다. 그렇기 때문에

템플릿 상속 기능을 사용하여 공통적인 내용을 담은 베이스 템플릿을 미리 작성하고 다른 템플릿들은 그 템플릿을

상속하는 것으로 보다 편리하게 HTML 표준을 적용할 수 있다.

 

먼저 공통 부분인 베이스 템플릿을 작성해보자.  이 템플릿은 pybo 앱에서만 사용될 템플릿이 아니기 때문에

templates 디렉토리에 바로 생성한 후 다음과 같이 작성한다.

위에서 설명했던 표준 HTML 구조에 따라 작성한 베이스 템플릿 파일 base.html 이다. body 엘리먼트 내의

{% block content %} 와 {% endblock %} 부분이 베이스 템플릿을 상속한 템플릿에서 개별적으로 구현해야 할 부분이다.

 

 

먼저 question_list.html 에서 base.html을 상속해보자.  파일을 다음과 같이 수정한다.

베이스 템플릿에 정의된 중복 부분 ({% load static %} 이나 css파일 등)이 제거되고 {% extentds 'base.html' %} 로

베이스 템플릿을 상속한 뒤 베이스 템플릿의 body 엘리먼트에 있었던 {% block content %} {% endblcok %} 으로

전체 내용을 감싸준 것을 확인할 수 있다.  이렇게 작성하면 베이스 템플릿의 {% block content %} ~ {% endblcok %}

부분에 question_list.html 의 내용이 들어간 것과 같은 상태가 된다.

 

마찬가지로 question_detail.html 도 다음과 같이 수정하여 HTML 표준을 적용시켜준다.

 

 

3. 표준 HTML 구조에 따른 웹페이지 개발의 이점

이것으로 지금까지 작성한 템플릿들에 표준 HTML 구조를 적용시키는 작업이 완료되었다.  물론 이 작업이 끝나도 웹페이지의 형태나 기능이 달라진 것은 아니지만 HTML 표준을 적용시킨 것으로 다음과 같은 이점을 얻을 수 있다.

 

① 웹 페이지의 구조와 표현(디자인 등)을 분리하는 것으로 두 요소를 독립적으로 구현하고 관리할 수 있게 됨

② 구조와 표현의 분리하는 것으로 유지, 보수가 간편해지며 불필요한 마크업을 최소화, 페이지 로딩속도 향상

 

③ 올바른 마크업과 css를 사용하여 구 버전의 웹 브라우저에서도 의도한대로 컨텐츠가 표시되며 웹 표준을

    지원하는 최신 기기나 환경에서도 동일한 결과를 기대할 수 있음. (상/하위 호환성)

④ 웹 표준에 따라 구조화된 웹 페이지는 검색엔진이 보다 쉽게 찾아낼 수 있어 웹 페이지를 쉽게 노출시킬 수 있다.   

⑤ 코드를 효율적으로 작성하는 것으로 파일 사이즈를 축소하고 서버 저장 공간을 절약할 수 있다.

 

 

 

 

 

'개인 프로젝트 > Django-mysite' 카테고리의 다른 글

#14 내비게이션 바  (0) 2021.09.24
#13 데이터 저장 2  (0) 2021.09.23
#11 부트스트랩(Bootstrap)  (0) 2021.09.22
#10 스타일시트(Style Sheet)  (0) 2021.09.22
#9 데이터 저장 1  (0) 2021.09.21

 이전 글에서 스타일시트를 사용하여 웹 페이지를 디자인 하는 방법을 배웠지만 직접 스타일시트를 작성하여

웹 페이지를 디자인하는 것은 쉬운 일이 아니다.  부트스트랩은 웹 디자이너가 아닌 개발자도 보다 쉽게 그럴싸한

웹 디자인이 가능하도록 보조해주는 프레임워크이다. 

 

1. 부트스트랩 설치

이 글을 작성하는 현재 시점에서 부트스트랩의 최신 버전은 v5.1.1 이다.  하지만 강좌를 따라가는 중인 만큼 우선은

강좌에서 사용한 v4.6.0 을 사용하여 진행하기로 한다. 최신 버전을 사용하는 법에 대해서는 추후에 별도로 공부하여

정리를 해보기로 한다.

 

 먼저 부트스트랩 홈페이지(https://getbootstrap.kr/)에 접속하여 v4.6.x 문서를 클릭한다.

 

그 다음 좌측에서 download 탭을 클릭하여 이동한 뒤 하단의 Download 버튼을 누르면 압축파일 하나를 다운받을 수

있다. 압축 파일의 내용물 중 bootstarp.min.css 파일을 이전 글에서 생성했던 static 디렉토리에 옮기면 설치는 끝이다.

 

static 디렉토리에 위치시킨 부트스트랩 스타일시트

 

 

 

2. 부트스트랩 적용 - 질문 리스트 페이지

 이제 부트스트랩을 템플릿에 적용시킬 차례이다.  question_list.html 을 다음과 같이 수정한다.

먼저 이전 글에서 했던 것 처럼 스타일시트를 적용시킨 뒤, table 태그로 표를 생성, thead(table head) 태그로 각 열의

이름을 지정하고 tbody(table body) 태그 내에 thead 에 정의한 열 이름에 맞춰 순서대로 정보를 표시하도록 한다.

만약 질문이 없다면 질문 글이 없음을 표시한다. (세 열을 병합한 크기의 한 줄로 표시한다.) 

 

 

여기까지 적용하고 웹 페이지에 접속해보면  위와 같이 웹 페이지의 디자인이 상당히 괜찮아진 것을 확인할 수 있다.

 

 

3. 부트스트랩 적용 - 질문 상세 페이지

 다음은 질문 상세 페이지에 부트스트랩을 적용해보자. question_detail.html 을 다음과 같이 수정한다.

마찬가지로 스타일시트를 적용해준 뒤 템플릿 코드를 수정해준다.  div 태그에서 card 클래스를 사용하여 박스안에 내용을 표시하고 박스 우하단에 생성일시를 표시하는 형태이다.  

 

여기까지 적용하고나면 위와 같이 질문 상세 페이지도 깔끔한 모습이 된 것은 확인할 수 있다. 

 

여기서 사용된 것을 비롯해 부트스트랩의 많은 클래스들을 어느정도 익혀두면 큰 어려움 없이 심플한 웹 디자인이

가능해진다.  더 자세히 알아보려면 공식문서(https://getbootstrap.com/docs/4.6)를 참조하면 좋다.

 

'개인 프로젝트 > Django-mysite' 카테고리의 다른 글

#13 데이터 저장 2  (0) 2021.09.23
#12 템플릿 상속  (0) 2021.09.23
#10 스타일시트(Style Sheet)  (0) 2021.09.22
#9 데이터 저장 1  (0) 2021.09.21
#8 URL과 네임스페이스  (0) 2021.09.21

질문 리스트 페이지
질문 상세 페이지

 

 이전까지의 과정으로 위와 같이 질문 목록을 보여주는 페이지와 질문글 상세 페이지를 만들 수 있었다. 

이번엔 웹 페이지를 디자인적으로 좀 더 그럴싸한 형태로 다듬을 차례이다. 

 

1. 스태틱(static) 디렉토리 

 웹 페이지의 디자인을 손보기 위해 스타일시트 파일을 만들어 적용할 것이다.  그리고 스타일시트 파일을

저장하기 위해 스태틱 디렉토리가 필요하다.  먼저 config/settings.py 를 다음과 같이 수정한다.

STATICFILES_DIRS 라는 변수를 추가하여 static 디렉토리의 경로를 추가해주었다. 

 

 

다음으로 static 디렉토리를 settings.py 에 추가된 경로에 실제로 생성해준다.

템플릿 디렉토리와 마찬가지로, 스태틱 디렉토리 또한 앱 디렉토리 하위에 바로 static 디렉토리를 생성하면

settings.py 파일의 수정 없이도 자동으로 인식하지만 템플릿 디렉토리도 그랬듯이 앱끼리 공통적으로 사용하게될

스태틱 파일 등의 문제로 추천되지 않는다. 이 글에서는 프로젝트 하위에 static 디렉토리를 생성하고 그 안에서

앱마다 하위 디렉토리를 생성하는 방식을 취한다.

 

 

우선 static 디렉토리에 간단한 스타일시트를 생성해보자. 다음과 같이 style.css 파일을 생성한다.

textarea 의 너비를 100%로 하고 답변등록 버튼의 상단에 10픽셀의 마진을 주는 스타일 시트이다. 

 

 

2. 스타일시트(Style Sheet) 적용

이제 이 스타일 시트를 질문 상세 템플릿에 적용해야한다. question_detail.html 을 다음과 같이 수정한다.

static 파일을 사용하기 위해 {% load static %} 코드를 상단에 삽입하여 스태틱 파일들을 로드한 뒤, link 태그를 

사용하여 스타일시트를 적용한다.

 

다시 웹 페이지에 접속해보면 위와 같이 텍스트 영역의 너비가 창 크기 전체를 차지하고 답변등록 버튼과

텍스트 영역 사이에 마진이 생긴 것을 확인할 수 있다. 

'개인 프로젝트 > Django-mysite' 카테고리의 다른 글

#12 템플릿 상속  (0) 2021.09.23
#11 부트스트랩(Bootstrap)  (0) 2021.09.22
#9 데이터 저장 1  (0) 2021.09.21
#8 URL과 네임스페이스  (0) 2021.09.21
#7 템플릿(Template)  (0) 2021.08.16

1. 답변등록 폼

 질문글 리스트와 질문글 상세 페이지까지 구현이 끝났으니 드디어 질문 글에 답변을 등록하는 기능을 만들 차례이다.  

먼저 답변을 등록하기 위한 폼(form)을 추가해야한다.  답변은 질문 상세 페이지에서 댓글처럼 등록할 수 있도록 할

것이기 때문에 질문 상세 페이지(question_detail.html)에 다음과 같이 폼을 추가한다.

 

form 태그action 속성은 form 태그 내에서 입력받은 데이터를 전송할 url과 전송 방식(get or post)을

지정한다. 여기서는 pybo 앱의 answer_create 라는 별칭의 url을 데이터를 전송할 url로, 전송 방식은 post

지정하였다.

 

{% csrf_token %} 은 보안을 위한 코드이다. 제출된 form 이 실제 웹페이지에서 작성한 데이터가 맞는지를

가늠하기 위해 서버에서 발행한 토큰값과 비교하고 만약 비정상적인 방법으로 제출된 데이터라면 토큰이

일치하지 않아 오류가 발생할 것이다. form 태그에서 입력받는 모든 정보에 대해 보안을 보장하기 위해 form 태그

바로 아래에 이 코드를 위치시켜야 한다. 

 

text_area 태그는 답변의 내용을 작성할 텍스트 영역이다.  여기서는 이름을 content(내용)로 지정하고 최대

15줄까지 보여지도록 설정한다. 지정한 줄 수를 넘어가면 텍스트 영역은 자동으로 스크롤 바를 생성한다.

(15줄 이상 입력하지 못하는 것이 아니다.)

 

input 태그는 type 속성에 따라 여러 형태로 사용자의 입력을 받아들이기 위한 태그이다. 여기서는 답변의

내용을 제출하기 위한 submit 속성으로 지정하였다.  submit 속성의 버튼이 클릭되면 지금까지 form 에 입력된

모든 데이터는 action 속성에 지정된 url로 지정된 전송 방식에 따라 전송된다. 

 

 

2. URL 매핑, 뷰 함수 추가

 물론 위의 작업만을 수행하고 웹페이지에 접속하면 에러가 발생한다. action 태그에 pybo:answer_create 라는

url 별칭을 사용했지만 그런 url 매핑은 등록해두지 않았기 때문이다. 

 

pybo/urls.py 에서 다음과 같이 url 매핑을 추가한다.

pybo:answer_create 을 추가해두었으니 이제 url 매핑 문제는 해결했다.  그러나 이번에는 뷰 함수에

answer_create 라는 함수가 존재하지 않는다.  이 또한 추가해줄 필요가 있다.

 

pybo/views.py 에 다음과 같이 timezone 모듈을 import 하고 answer_create 함수를 추가해준다.

먼저 question = get_object_or_404(Question, pk=question_id) 로 답변을 등록할 질문 객체를 가져온다.

그 다음 question 의 answer_set에서 form에서 POST 방식으로 전송받은 데이터중 content에 해당하는 것을

내용으로, timezone 모듈로 얻은 현재시각을 생성 시간으로 하여 새로운 답변을 생성한다.  물론, Answer 모델을

직접 사용하여 답변을 등록할 수도 있다.  과정만 다를 뿐 결과는 동일하다. 답변 등록이 완료되면 redirect를

호출하여 답변을 등록한 질문의 상세페이지로 돌아간다.

 

여기까지 완료되었다면 다음과 같이 질문 상세페이지에서 답변 등록 폼이 보이며 답변의 등록 또한 가능하다.

하지만 아직까지는 답변을 등록해도 변하는 것이 없다.  등록된 답변을 보여주는 기능을 질문 상세 페이지에

추가하지 않았기 때문이다.

 

 

 

3. 답변 조회

 이제 질문 상세 페이지에 현재 질문에 달린 답변을 모두 보여주는 기능을 추가한다.

 

templates/pybo/question_detail.html 를 다음과 같이 수정한다.

question.answer_set 에 담긴 답변의 갯수와 각 답변의 내용을 보여주는 코드를 추가한 것을 알 수 있다.

 

여기까지 완료되었다면 다음과 같이 등록한 답변이 질문 상세페이지에 출력된다.

 

'개인 프로젝트 > Django-mysite' 카테고리의 다른 글

#11 부트스트랩(Bootstrap)  (0) 2021.09.22
#10 스타일시트(Style Sheet)  (0) 2021.09.22
#8 URL과 네임스페이스  (0) 2021.09.21
#7 템플릿(Template)  (0) 2021.08.16
#6 django admin  (0) 2021.08.16

1. URL 하드코딩

 question_list.html 파일을 살펴보면 템플릿 코드에서 url을 직접 만들어서 넣어주었다.  이러한 방식은 url에 수정사항이 발생할 때 마다 모든 url 코드를 찾아 수정해야하는 불편이 발생한다.  이를 해결하기 위해 url의 별칭을 사용한다.

 

먼저 pybo/urls.py 를 다음과 같이 수정한다.

각 url 매핑에 name 속성을 부여한 것을 볼 수 있다. 이제 이 url 매핑은 하드코딩할 필요 없이 별칭만으로 사용할 수 있다.  이제 템플릿에서 하드코딩한 url을 별칭으로 대체할 차례이다.

 

templates/pybo/question_list.html 을 다음과 같이 수정한다.

"/pybo/{{ question.id }}" 로 하드코딩 되어있던 url 링크가 "{% url 'detail' question.id %}" 로 변경된 것을 볼 수 있다.

이제 detail 에 해당하는 url이 수정되더라도 템플릿은 수정할 필요가 없어졌다.

 

 

2. URL 네임 스페이스

 그런데 만약 pybo 이외의 앱을 추가했을 때, 그 앱에서도 같은 별칭의 url을 사용한다면 위의 방식으로는 중복이

발생하는 문제가 생긴다.  이를 해결하기 위해서 각 앱의 url 매핑마다 네임 스페이스를 지정해야한다. 

 

pybo/urls.py 를 다음과 같이 수정한다.

app_name = 'pybo' 를 추가하여 현재 앱의 url 매핑을에 'pybo' 라는 이름을 붙였다.  이제 템플릿 파일에서 별칭을 사용한 코드도 수정해줘야 한다.

 

url 링크가 "{% url 'detail' question.id %}" 에서 "{% url 'pybo:detail' question.id %}" 로 변경된 것을 확인할 수 있다.

url 네임스페이스 : url 별칭 의 형태로 사용하는 것으로 다른 앱끼리의 별칭의 중복을 피하는 것이다.

'개인 프로젝트 > Django-mysite' 카테고리의 다른 글

#10 스타일시트(Style Sheet)  (0) 2021.09.22
#9 데이터 저장 1  (0) 2021.09.21
#7 템플릿(Template)  (0) 2021.08.16
#6 django admin  (0) 2021.08.16
#5 django 의 기본 요소 - Model 사용  (0) 2021.08.11

 트리에서 두 노드의 가장 가까운 공통 조상을 보다 효율적으로 구하기 위한 LCA 알고리즘에 대해 관련 문제를 해결하며 알아본다. 

 

1. 가장 가까운 공통 조상(https://www.acmicpc.net/problem/3584)

 가장 기본적인 LCA 문제이다.  노드의 수가 최대 10,000이며 쿼리의 수도 하나 뿐이기 때문에 효율적인 알고리즘을 생각할 필요 없이 간선 정보를 토대로 각 노드의 부모노드를 저장하고 쿼리로 주어진 두 노드로부터 부모노드를 탐색해나가며 각 노드의 모든 조상노드 리스트를 구하고, 두 리스트를 뒤에서부터 비교해나갔을 때 마지막으로 같았던 두 리스트의 값이 가장 가까운 공통 조상 노드가 된다.  이를 구현한 코드는 다음과 같다.

import sys

input = sys.stdin.readline


# 3584 가장 가까운 공통 조상
# 노드의 수가 적고 쿼리도 케이스당 하나 뿐이기 때문에
# 단순한 방식으로도 해결 가능
def sol3584():
    # 케이스별 정답 리스트
    answer = []
    for t in range(int(input())):
        # 노드의 수
        n = int(input())

        # 각 노드의 부모노드
        parent = [0] * (n + 1)

        # 부모노드와 자식노드를 입력받아 부모노드를 채운다
        for _ in range(n - 1):
            a, b = map(int, input().split())
            parent[b] = a

        # 가장 가까운 공통조상을 구할 두 노드
        u, v = map(int, input().split())

        # 두 노드의 조상 노드 리스트를 구한다.
        up, vp = [], []
        while u:
            up.append(u)
            u = parent[u]
        while v:
            vp.append(v)
            v = parent[v]

        # 두 노드의 조상 노드 리스트를 뒤집는다.
        up, vp = up[::-1], vp[::-1]

        # 루트 노드부터 시작하여 두 노드의 조상 노드가 서로 달라질 때 까지 탐색
        # 두 노드의 조상 노드가 마지막으로 같았을 때의 조상 노드가 가장 가까운 공통 조상 노드가 된다.
        res = 0
        for i in range(min(len(up), len(vp))):
            if up[i] == vp[i]:
                res = up[i]
            else:
                break

        # 정답 리스트에 결과 삽입
        answer.append(res)

    # 출력 형식에 맞춰 정답 리스트 반환
    return '\n'.join(map(str, answer))

 

하지만 위의 방법은 당연하게도 노드의 수와 쿼리의 수가 많아지면 쿼리마다 사용할 수 없다.

 

 

이번엔 다른 방식으로 문제를 해결해보자.  절차는 다음과 같다.

 

① 먼저 트리를 탐색하여 모든 노드의 깊이와 부모노드를 구한다.

② 주어진 두 노드의 깊이가 다르다면 보다 깊이 있는 노드가 다른 노드와 같은 높이가 될 때 까지 거슬러 올라온다.

③ 두 노드가 서로 같아질 때 까지 두 노드 모두 부모노드를 타고 거슬러 올라온다.

④ 두 노드가 서로 같아졌을 때, 그 노드가 가장 가까운 조상 노드가 된다.

 

이를 구현한 코드는 다음과 같다.

 

import sys

input = sys.stdin.readline


def sol3584():
    # 정답 리스트
    answer = []
    
    # 테스트케이스
    for t in range(int(input())):
        # 노드의 수
        n = int(input())
        
        # 그래프 생성, 부모 노드 구하기
        g = [[] for _ in range(n + 1)]
        parent = [0] * (n + 1)
        for _ in range(n-1):
            a, b = map(int, input().split())
            g[a].append(b)
            parent[b] = a
        
        # 루트 노드로부터 bfs탐색으로 각 노드의 깊이를 구한다
        depth = [0] * (n + 1)
        root = [i for i in range(1, n+1) if not parent[i]][0]
        
        q = [root]
        while q:
            nq = []
            for cur in q:
                for c in g[cur]:
                    depth[c] = depth[cur] + 1
                    nq.append(c)
            q = nq
            
        # 주어진 두 노드 u, v
        u, v = map(int, input().split())
        
        # 노드 u의 깊이가 노드 v의 깊이보다 크도록 한다.
        if depth[u] < depth[v]:
            u, v = v, u
            
        # 두 노드의 깊이차가 0이 될 때 까지 깊은 쪽 노드인 u를 끌어올린다
        while depth[u] - depth[v]:
            u = parent[u]
           
        # 두 노드가 서로 같아질 때 까지 두 노드를 끌어올린다
        while u != v:
            u, v = parent[u], parent[v]
          
        # u와 v가 서로 같아지면 u == v == 가장 가까운 공통 조상 노드 가 된다.
        answer.append(u)
       
    # 출력 형식에 맞춰 정답 리스트 반환
    return '\n'.join(map(str, answer))

이 방식 자체로는 보다 많은 노드와 쿼리 수가 주어지는 문제를 해결하기에 딱히 이전의 알고리즘보다 효율적이지 않다.  하지만 이 방식에 sparse table을 적용하면 얘기가 달라진다.  다음 문제에서 이 sparse table에 대해 다룬다.

 

2. 합성함수와 쿼리(https://www.acmicpc.net/problem/17435)

 함수 f(x)의 값들이 주어졌을때, fn(x) 가 f(f(f(...f(x)...))) 와 같이 함수 f를 n중첩한 결과라고 한다.  이 때, 주어진 m개의

n, x 쌍에 대해 fn(x) 를 각각 구하는 문제이다.  LCA 문제는 아니지만 LCA 알고리즘의 핵심인 sparse table을 응용해서

풀 수 있는 문제이다. 절차는 다음과 같다.

 

① 먼저 함수 f의 값을 저장할 2차원 배열을 생성한다.  이 때, f[x][k] 는 함수 f의 k중첩에 x를 넣은 결과이다.  예를들어,

    f[3][4] = f(f(f(f(3)))) 이 된다.

 

② f[x][0] 의 값을 입력받은 f(x)의 값으로 초기화한다.

 

③ f[i][j] 은 f[i][j-1] 을 2^(j-1) 중첩한 값이 되기 때문에, 점화식 f[i][j] = f[f[i][j-1]][j-1] 이 성립한다. 이 점화식에 따라 

    나머지 f[x][1~k] 를 채운다.  여기서 k는 함수 중첩수 최댓값인 n에 로그를 취한 뒤 올림한 값이 된다. 

    => math.ceil(math.log2(n))

 

④ 이제 쿼리로 n, x가 주어지면 ③ 에서 구한 sparse table을 이용하여 빠르게 목표한 값으로 접근한다. 

    n이 0보다 큰 동안, x = f[x][int(log2(n))];  n -= (1 << int(log2(n))) 을 수행하면 가능하다.  n 중첩을

    1씩 좁혀나가는 것이 아니라 2의 승수단위로 좁혀나가는 것으로 탐색시간을 극적으로 줄이는 방식이다.

 

⑤ ④의 작업이 모두 끝났을 때, x의 값이 해당 쿼리에 대한 정답이 된다.

import sys
from math import log2, ceil

input = sys.stdin.readline



def sol17435():
    # 주어진 함수값의 수
    m = int(input())
    
    # 500000 <= 2^k
    k = ceil(log2(500000))
    
    # sparse table :  f[x][k] = fk(x)
    f = [[-1] * k for _ in range(m + 1)]
    fn = list(map(int, input().split()))
    for i in range(m):
        f[i + 1][0] = fn[i]

    # 점화식 f[i][j] = f[f[i][j-1]][j-1] 에 따라 sparse table 나머지 채우기
    for j in range(1, k):
        for i in range(1, m + 1):
            f[i][j] = f[f[i][j - 1]][j - 1]

    # 쿼리의 수
    q = int(input())
    
    # 각 쿼리의 정답 구하기
    answer = []
    for _ in range(q):
        # 함수 중첩수 n, 인자값 x
        n, x = map(int, input().split())
        
        # 중첩수가 0이 되면 종료
        while n:
            # f[x][d] == f2^d(x)
            d = int(log2(n))
            x = f[x][d]
            
            # 2^d 중첩만큼 처리했기 때문에 남은 중첩수에서 감산
            n -= (1 << d)
            
        # 정답리스트에 쿼리의 정답 저장
        answer.append(x)
       
    # 출력 형식에 맞춰 정답 리스트 반환
    return '\n'.join(map(str, answer))

 sparse table은 말그대로 어떤 규칙에 따른 값을 지수단위로 듬성듬성하게 구해두고 원하는 값까지 로그시간으로

빠르게 접근하기 위한 자료구조이다.  이 문제의 경우 sparse table 은 구성하는데에 O(MlogN)의 시간복잡도를,

쿼리 하나를 처리하는데 O(logN) 의 시간복잡도를 보인다.(경우에 따라서는 O(1) 수준의 성능을 보일때도 있다고 한다.)

sparse table은 정적인 데이터에서의 쿼리만을 처리할 수 있기 때문에 동적 데이터(데이터가 삽입, 삭제, 변경되는 경우)

에는 대응이 불가능하다.  만약 정적인 데이터에 대해 대량의 쿼리를 처리해야하는 경우라면 sparse table을 사용하는

것을 고려해볼만 할 것 같다.   

 

 

3. LCA 2 (https://www.acmicpc.net/problem/11438)

 이제 1번 문제에서 두 번째로 사용했던 알고리즘에 sparse table을 적용할 차례다.  노드의 수가 최대 100,000개로

늘어나고 쿼리의 수도 최대 100,000개이기 때문에 O(NM)의 시간복잡도를 보이는 기존의 방식으로는 해결할 수 없다.

두 노드의 깊이를 동일하게 맞추는 작업과 동일한 깊이에 있는 두 노드의 가장 가까운 조상 노드를 찾아가는 작업을 

sparse table을 사용하여 O(logN) 수준으로 최적화한다.  이를 구현한 코드는 다음과 같다.

 

import sys
import math

input = sys.stdin.readline


# sparse table(희소 테이블)을 활용하여 문제를 더 효율적으로 해결할 수 있다. 절차는 다음과 같다.
# 1. 각 노드의 깊이를 나타낼 depth 리스트와 부모노드를 나타낼 parent 리스트를 생성한다
#    이 때, parent 리스트는 2차원 리스트이며 parent[i][k] 는 노드 i의 2^k 번 거슬러올라간 조상노드를 의미한다.
#
# 2. dfs 혹은 bfs 로 트리를 순회하며 각 노드의 1번 거슬러올라간 조상노드(parent[i][0])와 깊이를 구한다.
#    이 때, 점화식 parent[i][j] = parent[parent[i][j-1]][j-1] 를 사용해서 O(NK) 의 시간 복잡도로 작업을 수행한다.
#    K는 log(2, N) 을 올림한 값과 같다.
#
# 3. 각 질의(LCA 를 구할 두 노드 u, v) 에 대해 두 노드의 깊이가 같아질 때 까지
#    보다 깊이가 큰 노드를 끌어올린다.
#
# 4. 두 노드가 서로 같다면 그대로 어느 한쪽 노드를 출력
#
# 5. 그렇지 않다면 두 노드의 가장 먼 공통 조상으로부터 처음으로 두 노드의 조상 노드가 달라질 때 까지 탐색
#    탐색 종료 후 노드 u 또는 v의 부모노드를 출력
def sol11438():
    # 각 노드의 깊이와 첫 번째 부모를 구하기 위한 탐색 함수
    def bfs(root):
        q = [root]
        while q:
            nq = []
            for cur in q:
                for c in g[cur]:
                    if depth[c] < 0:
                        depth[c] = depth[cur] + 1
                        parent[c][0] = cur
                        nq.append(c)
            q = nq

    # 노드의 수
    n = int(input())

    # sparse table 의 크기
    k = math.ceil(math.log2(n))

    # 그래프 생성
    g = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        a, b = map(int, input().split())
        g[a].append(b)
        g[b].append(a)

    # 각 노드의 깊이 리스트
    depth = [-1] * (n + 1)

    # 각 노드의 조상 노드 리스트 (sparse table)
    parent = [[-1] * k for _ in range(n + 1)]

    # 루트 노드인 1의 깊이를 0으로 하고 탐색 시작
    depth[1] = 0
    bfs(1)

    # sparse table 채우기
    for j in range(1, k):
        for i in range(1, n + 1):
            parent[i][j] = parent[parent[i][j - 1]][j - 1]

    # 각 질의에 대해 정답 구하기
    answer = []
    for _ in range(int(input())):
        u, v = map(int, input().split())

        # 노드 u가 노드 v 보다 깊이가 크도록 한다
        if depth[u] < depth[v]:
            u, v = v, u

        # 두 노드의 깊이가 같아질 때 까지 노드 u를 끌어올린다
        while depth[u] - depth[v]:
            u = parent[u][int(math.log2(depth[u] - depth[v]))]

        # 두 노드가 같지 않다면
        if u != v:
            # 가장 먼 공통 조상 노드로부터 처음으로 두 노드의 조상 노드가 달라질 때 까지 탐색
            for j in range(math.ceil(math.log2(depth[u])), -1, -1):
                if parent[u][j] != parent[v][j]:
                    u = parent[u][j]
                    v = parent[v][j]

            # 현재 u와 v는 공통 조상노드의 자식 노드인 상태이므로 둘 중 하나는 부모노드를 방문
            u = parent[u][0]

        # 노드 u 혹은 v를 반환
        answer.append(u)

    # 출력 형식에 맞춰 정답리스트 반환
    return '\n'.join(map(str, answer))

 

 지난 글에서 위상 정렬을 구현하는 방법으로 진입 차수 테이블과 큐를 사용한 방법을 알아보았다.  이번에는

위상 정렬을 구현하는 다른 방법인 dfs(깊이우선탐색)를 활용한 방법을 알아보고 몇 개의 문제를 해결해본다.

 

1. 줄 세우기(https://www.acmicpc.net/problem/2252) - 구현

 진입 차수 테이블과 큐를 사용한 방법의 경우, 진입 차수가 0인 노드를 모드 큐에 넣으며 탐색해나가는

bfs(너비우선탐색)의 형태로 구현된다. 그리고 그 방식에서는 큐에서 뽑혀 탐색된 노드 순서가 곧 정렬 순서가 되었다. dfs를 활용한 방식에서는 정확히 그 반대라고 생각하면 편하다. 즉, 탐색이 가장 먼저 종료된 순서의 역순이 곧

정렬 순서가 된다.  이 방식으로 줄 세우기 문제를 해결한 코드는 다음과 같다.

 

import sys
sys.setrecursionlimit(100000)

input = sys.stdin.readline


def sol2252():
    # 학생 수 n, 키 비교 결과 수 m
    n, m = map(int, input().split())
    
    # 그래프, 진입 차수 테이블 생성
    # bfs 형태의 구현과 달리 진입 차수 테이블은 생성이후 수정할 일이 없음
    g = [[] for _ in range(n + 1)]
    degree = [0] * (n + 1)
    for _ in range(m):
        a, b = map(int, input().split())
        g[a].append(b)
        degree[b] += 1
    
    # dfs를 활용한 위상 정렬 구현
    def dfs(d):
        nonlocal answer
        # 현재 노드 방문처리
        visit[d] = True
        
        # 아직 방문하지 않은 자식노드 방문
        for child in g[d]:
            if not visit[child]:
                dfs(child)
               
        # 탐색이 종료된 노드를 answer에 삽입
        answer.append(d)
        
    # 방문여부 리스트
    visit = [False] * (n + 1)
    
    # 정렬 결과 리스트
    answer = []
    
    # 처음부터 진입 차수가 0인 노드로부터 탐색 시작 
    for i in range(1, n + 1):
        if not degree[i]:
            dfs(i)
            
    # 정렬 결과 리스트를 뒤집어 출력 형식에 맞춰 반환
    return ' '.join(map(str, answer[::-1]))

 

 

2. ACM Craft (https://www.acmicpc.net/problem/1005)

 건설 규칙과 건물을 완성하는데 걸리는 시간이 주어졌을 때, 특정 건물을 건설하는데 걸리는 최소시간을 구하는 문제.

건물을 노드, 건설 규칙을 간선으로 생각하면 위상 정렬로 쉽게 해결할 수 있는 문제이다.  다만 각 노드(건물)까지

소요되는 최소 시간을 매번 다시 계산할 순 없기 때문에 간단한 동적계획법을 적용할 필요가 있다. 이를 해결한 코드는

다음과 같다.

 

import sys

sys.setrecursionlimit(100000)
input = sys.stdin.readline


def sol1005():
    # dfs(d) = dp[d] 는 d 번 건물을 건설하기 위해 필요한 최소 비용
    def dfs(d):
        # 아직 건물 d를 건설하기 위한 최소비용이 계산되지 않았다면 계산에 들어간다.
        # 건물 d를 건설하기 위한 최소비용은 반드시 먼저 건설되어야할 건물의 최소 비용 중 
        # 가장 큰 비용에 건물 d를 건설하기 위해 필요한 비용을 합친 값이 된다.
        if dp[d] == -1:
            dp[d] = max([dfs(p) for p in g[d]], default=0) + cost[d]
            
        # 건물 d를 건설하기 위한 최소 비용을 반환
        return dp[d]

    # 케이스별 정답 리스트
    answer = []
    for t in range(int(input())):
        # 건물 수 n, 건설 규칙 수 k
        n, k = map(int, input().split())
        
        # 각 건물을 완성하는데 필요한 시간
        cost = [0, *map(int, input().split())]

        # 그래프를 생성
        g = [[] for _ in range(n + 1)]
        for _ in range(k):
            x, y = map(int, input().split())
            
            # 이 문제의 경우 정확하게 건물 w를 짓는데 드는 최소 비용만을 구하는 문제
            # 노드 전체를 위상정렬하는 문제와 다르게 건물 w를 짓기 위해 지어야 하는 
            # 건물들의 최소 비용만을 탐색하는 것이 효율적이기 때문에
            # 역탐색을 위해 그래프의 인접리스트에 갈 수 있는 노드가 아닌 
            # 현재 노드로 올 수 있는 노드를 저장한다.
            g[y].append(x)

        # dfs(w)를 구하여 정답 리스트에 저장
        dp = [-1] * (n + 1)
        w = int(input())
        answer.append(dfs(w))

	# 출력 형식에 맞춰 정답 리스트 반환
    return '\n'.join(map(str, answer))

 

 

 위상 정렬의 개념과 이를 활용하여 해결할 수 있는 문제를 알아본다.

 

1. 위상 정렬(Topological Sorting)

 위상 정렬은 간단히 말하자면 순서를 가진 항목들을 줄 세우는 정렬방식이다.  예를 들어 1은 반드시 3보다 앞에 오고

2는 반드시 4보다 앞에 온다고 해보자,  이 때, [1, 2, 3, 4]를 위상 정렬한 결과는 [1, 3, 2, 4], [1, 2, 3, 4], [2, 4, 1, 3],

[2, 1, 4, 3] 모두가 될 수 있다.  즉, 주어진 항목간의 순서를 유지하는 것을 전제로 정렬을 수행하는 것이다.  주로 어떤

작업들을 수행되어야 할 순서에 따라 배치하기 위해 사용되는 알고리즘이다.  정렬의 대상은 기준이 될 명확한 순서가

존재해야 하기 때문에 당연히 방향성을 가졌으며 사이클이 존재하지 않는 그래프의 형태로 나타낼 수 있어야 한다.

 

2. 줄 세우기(https://www.acmicpc.net/problem/2252) - 위상 정렬의 구현

 학생의 수(노드의 갯수)와 학생들의 키를 비교한 결과(간선)가 주어졌을 때 학생들을 키순서로 줄 세우는 문제

위상 정렬의 구현을 연습할 수 있는 문제이다. 구현의 절차는 다음과 같다.

 

① 우선 위상 정렬을 위해 필요한 것은 그래프진입 차수 테이블이다.  주어진 노드의 갯수와 간선을 이용하여

    이 두 가지를 먼저 구해야한다. 노드 X의 진입 차수는 X로 직접 올 수 있는 길을 가진 노드의 갯수를 의미한다.

 

② 진입 차수가 0인 노드를 순서에 상관없이 모두 에 집어넣는다.

 

③ 큐에서 노드를 꺼내서 해당 노드로부터 뻗은 간선을 모두 제거하고 진입 차수 테이블을 갱신한다.

 

④ ②, ③ 작업을 큐가 빌 때 까지 반복한다.

 

⑤ 작업이 끝났을 때, 큐에서 노드를 꺼낸 순서가 곧 위상 정렬된 순서가 된다.

 

이를 구현한 코드는 다음과 같다.

 

import sys

input = sys.stdin.readline


def sol2252():
    # 학생의 수 n, 비교 결과의 수 m
    n, m = map(int, input().split())
    
    # 학생을 노드로, 비교 결과를 간선으로 하여 그래프로 표현
    # 각 노드(학생)의 진입 차수 테이블을 생성
    g = [[] for _ in range(n + 1)]
    degree = [0] * (n + 1)
    for _ in range(m):
        a, b = map(int, input().split())
        degree[b] += 1
        g[a].append(b)
        
    # 진입 차수가 0인 모든 노드를 큐에 삽입
    q = [i for i in range(1, n + 1) if not degree[i]]
 
 	# 정렬 결과 리스트
    answer = []
    
    # 큐가 빌 때 까지
    while q:
        nq = []
        # 큐에서 노드를 하나씩 꺼내 정렬 결과 리스트 answer 에 삽입
        # 해당 노드로부터 뻗은 간선을 제거하고 진입 차수를 갱신
        # 그 결과 진입 차수가 0이 된 노드를 모두 큐에 삽입
        for num in q:
            answer.append(num)
            for child in g[num]:
                degree[child] -= 1
                if degree[child] == 0:
                    nq.append(child)
        q = nq
        
    # 출력 형식에 맞춰 정렬 결과를 반환
    return ' '.join(map(str, answer))

 

 

3. 최종 순위(https://www.acmicpc.net/problem/3665)

 작년의 팀들의 순위, 올해 상대적 순위가 변경된 팀의 순서쌍이 주어졌을 때, 올해 팀들의 순위를 구하는 문제.

역시 위상 정렬을 사용하여 해결 가능하지만 작년의 순위와 변경된 순서쌍만으로 순위를 추측해야한다. 주의할 점은 

주어진 순서쌍의 순서는 실제로 변경된 순위와는 관계가 없다는 것이다.  예를들어, 작년의 순위가 1, 2 였을 때,

순서쌍이 1, 2로 주어지든 2, 1로 주어지든 최종 순위는 2, 1이 되어야 한다.

 

 문제를 해결하는 절차는 다음과 같다.

 

① 기존 순위를 토대로 간선을 생성한다.  예를들어 기존 순위가 5 4 3 2 1 일 경우, 간선은

    (5, 4), (5, 3), (5, 2), (5, 1), (4, 3), (4, 2), (4, 1), (3, 2), (3, 1), (2, 1) 이 된다.

 

② 생성한 간선으로 그래프와 진입 차수 테이블을 생성한다.

 

③ 순위가 변경된 팀의 순서쌍에서 더 낮은 순위였던 쪽을 a, 더 높은 순위였던 쪽을 b로 하여 그래프와

    진입 차수 테이블을 수정한다.

 

④ 그래프에 대해 위상 정렬을 수행한다.

 

⑤ 위상정렬의 수행한 결과 리스트의 길이가 총 노드의 갯수에 미치지 못할 경우 그래프에 사이클이 존재한다는

    의미이기 때문에 데이터에 일관성이 없어 순위를 정할 수 없는 경우에 속하므로 IMPOSSIBLE을 출력한다

 

⑥ 그렇지 않다면 정상적으로 위상 정렬이 완료된 것이기 때문에 정렬 결과를 출력한다.

 

이를 구현한 코드는 다음과 같다.

 

import sys

input = sys.stdin.readline


def sol3665():
    # 케이스별 정렬 결과 리스트
    answer = []
    
    for t in range(int(input())):
        # 팀의 수(노드의 수)
        n = int(input())
        
        # 작년 순위에 따라 그래프와 차수 테이블 생성
        g = [set() for _ in range(n + 1)]
        degree = [0] * (n + 1)
        teams = list(map(int, input().split()))
        for i in range(n):
            a = teams[i]
            for j in range(i + 1, n):
                b = teams[j]
                g[a].add(b)
                degree[b] += 1

        # 변경된 순위에 따라 그래프와 차수 테이블 수정
        m = int(input())
        for _ in range(m):
            a, b = map(int, input().split())
            
            # 보다 낮은 순위였던 쪽이 a가 되게 한다
            if a not in g[b]:
                a, b = b, a
               
            g[b].discard(a)
            g[a].add(b)
            degree[a] -= 1
            degree[b] += 1
            
        # 정렬 결과
        res = []
        
        # 위상 정렬 수행
        q = [i for i in range(1, n + 1) if not degree[i]]
        while q:
            nq = []
            for num in q:
                res.append(num)
                for child in g[num]:
                    degree[child] -= 1
                    if not degree[child]:
                        nq.append(child)
            q = nq

		# 사이클이 발견된 경우 -> 일관성이 없어 정렬이 불가능한 경우
        if len(res) != n:
            answer.append('IMPOSSIBLE')
            
        # 정렬이 정상적으로 수행된 경우
        else:
            answer.append(' '.join(map(str, res)))

	# 출력 형식에 맞춰 정렬 결과 반환
    return '\n'.join(answer)

 

 

4. 문제집(https://www.acmicpc.net/problem/1766)

 문제집의 1~N까지의 문제를 풀 순서를 다음 세가지 조건에 따라 정하는 문제

1. N개의 문제는 모두 풀어야한다

2. 먼저 푸는 것이 좋은 문제가 있는 문제는 먼저 푸는 것이 좋은 문제를 반드시 먼저 풀어야 한다.

3. 가능하면 쉬운 문제부터 풀어야 한다. (문제 번호가 낮을수록 난이도가 낮다)

 

순서를 지켜야한다는 점에서 알 수 있듯이 위상정렬을 사용하여 해결 가능한 문제이다.  다만 같은 단계에서

진입 차수가 0이라면 순서가 상관없었던 기존의 위상정렬로는 3번 조건을 만족시킬 수 없다.  여기서는 

기존의 큐를 heapq 로 대체하는 것으로 문제를 해결할 수 있다. 구현은 다음과 같다.

 

import sys
from heapq import heappush, heappop, heapify

input = sys.stdin.readline


def sol1766():
    # 문제의 갯수 n, 먼저 푸는게 좋은 문제의 정보의 갯수 m
    n, m = map(int, input().split())
    
    # 그래프, 진입 차수 테이블 생성
    g = [[] for _ in range(n + 1)]
    degree = [0] * (n + 1)
    for _ in range(m):
        a, b = map(int, input().split())
        g[a].append(b)
        degree[b] += 1

    # 최초에 진입 차수가 0인 노드(문제)를 큐에 담는다
    q = [i for i in range(1, n + 1) if not degree[i]]
    
    # 큐를 heapq로 변환
    # Python의 heapq는 기본적으로 최소 힙이기 때문에
    # 더 작은 숫자, 즉 더 쉬운 문제부터 뽑게된다.
    heapify(q)
    
    # 정렬 결과 리스트
    answer = []
    
    # 위상 정렬 실행
    # 기존의 방식과 같지만 큐에서 값을 꺼내고 다시 넣는 동작을
    # heappop 과 heappush를 사용하여 수행한다.
    # 이것으로 매번 가능하면 가장 쉬운 문제를 해결하는 3번 조건을 만족시킨다.
    while q:
        cur = heappop(q)
        answer.append(cur)
        for c in g[cur]:
            degree[c] -= 1
            if not degree[c]:
                heappush(q, c)

    # 출력 조건에 따라 정답 리스트 반환
    return ' '.join(map(str, answer))

 heap을 사용하면 항상 정렬된 것과 마찬가지인 상태를 유지할 수 있다는 점만 떠올리면 쉽게 해결할 수 있는 문제였다.

 

 

위상정렬의 구현은 다음에 알아볼 dfs를 사용한 구현이 보다 간단하지만 큐를 사용한 구현이 풀이에 적합한 경우도 있기 때문에 두 방법 모두 숙지해둘 필요가 있다.

 누적 합은 2차원 배열에서도 1차원에서와 거의 동일하게 활용 가능하다.  이번에는 2차원 배열에서의

누적 합의 성질과 이를 활용하여 풀 수 있는 문제들을 알아본다.

 

1. 2차원 배열의 누적합 구하기

 1차원 배열에서는 단순히 이전 인덱스의 값을 더해주면 끝이었지만 2차원 배열에서 누적합을 구하려면 각 행의

누적 합과 각 열의 누적 합을 구하는 두 단계를 거쳐야한다.  예시로 2차원 배열 하나의 누적 합을 구해보자.

 

① 먼저 각 행의 누적합을 구한다.

 

② 다음으로 각 열에 대한 누적 합을 구한다.

 

③ 2차원 행렬에서의 누적합 배열이 완성된다.  ①과 ②의 순선느 서로 뒤바뀌더라도 문제가 없다.

 

 

2. 2차원 누적 합의 성질

 2차원 배열에서의 부분 합을 구하거나 여러 구간에 대한 증감 적용을 한꺼번에 처리하는 데

활용할 수 있는 성질들을 가지고있다. 

 

누적합 S(r, c)는 2차원 배열 A에 대해 다음과 같은 성질을 가진다.

① S(r, c) = sum([A[i][j] for i in range(r) for j in range(c)]) 

② S(0, 0) = A[0][0]

③ S(r, c) = S(r-1, c) + A[r][c]

④ sum([A[i][j] for i in range(r1, r2+1) for j in range(c1, c2+1)]) = S(r2, c2) + S(r1-1, c1-1) - S(r1, c2-1) - S(c1-1, r2)

② 번 성질이 성립하는 이유

1차원 배열에서의 누적 합과 조금 다르지만 거의 유사한 성질을 가진 것을 알 수 있다.

 

 

3. 2차원 배열의 합

N*M 크기의 2차원 배열이 주어졌을때 K 개의 특정 구간에 대한 부분 합을 구하는 문제.  단순히 매번 반복문을 돌려 합을 구할 경우 O(NMK)의 시간복잡도를 보인다.  하지만 누적합의 4번 성질을 이용하면 O(NM+K)만에 해결할 수 있다.

문제를 해결한 코드는 다음과 같다.

 

import sys

input = sys.stdin.readline


def sol2167():
    # 행렬의 크기 n, m
    n, m = map(int, input().split())
    
    # 주어진 행렬 seq
    # 누적 합의 성질 4번을 사용할 때, 0번 인덱스의 이전 인덱스를 참조할 경우를 대비해 
    # 행/열의 끝에 0을 추가
    seq = [[*map(int, input().split()), 0] for _ in range(n)]
    seq.append([0] * m)
    seq.append([0] * m)
    
    # 2차원 배열의 누적합 배열을 구한다
    for i in range(n):
        for j in range(m):
            # 현재 위치의 누적합을 구하기 전에 같은 열의 다음 행의 값에 
            # 현재위치의 값을 더해주는 것으로 열에 대한 누적합을 동시에 계산해나갈 수 있다.
            seq[i + 1][j] += seq[i][j]
            seq[i][j] += seq[i][j - 1]

	# 정답 리스트
    answer = []
    
    # 누적 합의 성질 4번을 사용하여 부분 합을 구하고 정답 리스트에 저장
    for _ in range(int(input())):
        i, j, x, y = map(int, input().split())
        answer.append(seq[x-1][y-1] + seq[i - 2][j - 2] - seq[x-1][j - 2] - seq[i - 2][y-1])

    # 출력 형식에 맞춰 정답 리스트 반환
    return '\n'.join(map(str, answer))

 

 

4. 

 누적 합(prefix sum)은 그 특성상 배열의 부분합을 빠르게 구하는데 굉장히 유용하다. 이번에는 1차원 배열에서의 누적 합의 성질과 이를 이용하여 해결 가능한 문제들을 다뤄본다.

 

1. 누적 합의 성질

 어떤 1차원 배열 A = [a(1), a(2), ... , a(n)] 에 대해, 누적합 S(k)는 다음과 같은 성질을 가진다.

 ① S(k) = a(1) + a(2) + ... + a(k)

 ② S(1) = a(1)

 ③ S(k) = S(k-1) + a(k)

 ④ a(i) + a(i+1) + ... + a(j-1) + a(j) = S(j) - S(i-1) 

 

 

2. 구간 합 구하기 4(https://www.acmicpc.net/problem/11659)

 매우 간단한 누적합 활용문제이다.

import sys

input = sys.stdin.readline


def sol11659():
    # 수열의 크기 n, 질의의 갯수 m
    n, m = map(int, input().split())

    # n개의 수로 이루어진 수열
    seq = [0, *map(int, input().split())]

    # 수열의 누적합 전처리
    for i in range(1, n + 1):
        seq[i] += seq[i - 1]

    # 각 질의에 대해 정답을 계산하여 answer 에 삽입
    answer = []
    for _ in range(m):
        i, j = map(int, input().split())
        answer.append(seq[j] - seq[i - 1])

    # 출력형식에 맞춰 정답리스트 반환환
    return '\n'.join(map(str, answer))

a(i) + a(i+1) + ... + a(j-1) + a(j) = S(j) - S(i-1) 임을 이용하여 문제를 해결하였다.

 

 

3. 광고 삽입(https://programmers.co.kr/learn/courses/30/lessons/72414)

 영상 전체의 길이와 시청자들이 어느 구간을 재생했는지의 로그들이 주어질 때, 광고를 삽입할 최적의 위치를 찾기위해 가장 재생횟수가 많은 구간의 시작시간을 구하는 문제이다.  만약 재생횟수가 가장 많은 구간이 여러개라면 그 중 가장 빠른 시작시간을 반환한다. 위의 문제보다는 복잡하지만 역시 누적 합을 활용하여 해결할 수 있는 문제이다. 

 

주어지는 데이터는 동영상 전체의 재생시간 play_time 과 광고의 재생시간 adv_time, 영상의 재생구간 로그들이 담긴 리스트 logs이다.  문제를 해결하는 과정은 다음과 같다.

 

① 먼저, 제한사항에 광고의 재생시간은 동영상의 재생시간보다 짧거나 같게 주어진다는 조건이 있다. 

    광고의 재생시간이 영상의 재생시간보다 짧을 경우에는 평범하게 최적의 광고시간을 탐색하면 그만이지만

    같을 경우에는 애초에 탐색할 필요가없다.  영상의 시작시간이 곧 광고의 시작시간이 될 수 밖에 없기 때문이다.

 

② play_time 과 adv_time은 'HH:MM:SS' 의 문자열 형태이기 때문에 다루기 쉽도록 정수형태로 파싱해줄 필요가 있다.

    가장 작은 초단위로 시간을 변환한다. HH * 3600 + MM * 60 + SS 로 간단하게 변환 가능하다.

 

③ 다음은 광고가 삽입될 최적의 위치를 찾기위해 로그를 분석해야한다.  가장 먼저 떠오르는 방식은 1초에 한칸씩

    0으로 초기화된 배열을 할당하여 재생된 부분마다 1씩 더해주는 것으로 가장 재생수가 많은 구간을 탐색하는

    것이다.  다행히도 play_time의 범위가 00:00:00 부터 99:59:59,  즉, 0초부터 359999초 까지라는 조건이 있기 때문에

    배열의 형태로 충분히 나타낼 수 있다.

 

④ 예를 들어 로그가 '00:00:00-00:00:04',  '00:00:02-00:00:07', '00:00:06-00:00:10' 의 세 개라고 생각해보자.

    문제의 제한사항에 "예를 들어, 00시 00분 01초부터 00시 00분 10초까지 동영상이 재생되었다면, 동영상

    재생시간은 9초 입니다." 라는 조건이 있다. 즉, 종료시간은 재생구간에 들어가지 않는다는 것이다.

    이에 따라 위 로그를 배열상에 표현하면 아래와 같은 형태가 된다.

    문제는 여기서 발생한다. 로그 한번마다 구간내의 값을 모두 1 씩 증가시키려면 재생시간의 길이가 N, 로그의 갯수가

    M일 때 최악의 경우(모든 로그의 범위가 영상의 재생시간과 같을 경우) O(NM) 의 시간복잡도를 보인다.

    이 문제에서 N은 최대 360000, M은 최대 30만의 크기를 가지기에 이 알고리즘은 매우 비효율적이다.

   

⑤ 여기서 우리는 누적 합의 성질 중 하나인 S(k) = S(k-1) + a(k) 를 활용할 수 있다.  이번엔 로그의 시작 시간과

    종료 시간에만 시청횟수의 증감을 더해보자.  그러면 다음과 같은 형태가 된다.

    각 로그의 시작 시간인 0, 2, 6초에는 +1을, 종료 시간인 4, 7, 10초에는 -1을 더하였다. 이제 위 테이블에서

    누적합의 성질에 따라 누적합을 구해보자. 그러면 다음과 같은 형태가 된다.

    한눈에 봐도 ④ 에서 구했던 로그마다 전 구간의 값을 1씩 증가시키는것으로 얻었던 배열과 같음을 알 수 있다.

    게다가 이 방식은 O(N+M)의 시간복잡도로 위의 O(NM)의 알고리즘과 같은 결과를 얻을 수 있다.  이와 같이

    누적 합을 활용하면 어떤 배열에 여러 구간에 대한 증/감을 반영하는 작업을 굉장히 효율적으로 처리할 수 있다.

 

⑥ 여기까지 구하고나면 다음은 어렵지 않다.  위에서 구한 배열에서 영상의 시작시간부터 시작하여 광고 시간만큼의

    구간의 합이 최대치가 되는 구간의 시작시간을 구하면 된다. 최댓값이 여러개라면 가장 빠른 시간을 구하라는 조건도

    처음 찾은 최댓값이 더 큰 값이 오기 전에는 갱신되지 않도록 하면 자동으로 충족된다.  여기서 구간합이 최대가 되는

    경우를 찾는 것도 두 가지 방법이 있다.

 

⑦ 하나는 마찬가지로 누적 합을 이용한 방법이다.  a(i) + a(i+1) + ... + a(j-1) + a(j) = S(j) - S(i-1) 임을 이용하여,

    위 배열의 누적합을 다시한번 구한 뒤 구간마다 구간합을 빠르게 구할 수 있다.

 

⑧ 다른 하나는 투 포인터를 활용하는 방법이다.  첫 번째 구간의 합만을 구한 뒤, 다음 구간으로 넘어갈때마다

    첫 번째 요소를 빼고 새로운 요소를 더하는 식으로 구간합을 갱신해나갈 수 있다.

 

⑨ 마지막으로 구한 시작시간을 다시 'HH:MM:SS' 의 형태의 문자열로 변환하여 반환하면 된다.

 

위 절차에 따라 문제를 해결한 코드는 다음과 같다.

 

1) 누적 합의 성질을 활용하여 최대 구간합을 구한 방식 (⑦)

def solution(play_time, adv_time, logs):
    # ① 광고의 재생시간이 영상의 재생시간과 같다면 영상의 첫 부분에 삽입하면 된다.
    if play_time == adv_time:
        return '00:00:00'
    
    # ② 영상 재생시간과 광고 재생시간을 초단위의 정수로 변환
    pt, at = stoh(play_time), stoh(adv_time)
    
    # ③ 시간별 재생횟수를 나타내기위한 타임라인 배열을 생성
    timeline = [0] * 360001
    
    # ④, ⑤ 로그를 분석하여 재생 시작시간과 종료시간을 구한 뒤
    # 누적 합을 활용하여 타임라인에 재생 횟수를 표기
    for log in logs:
        s, e = map(stoh, log.split('-'))
        timeline[s] += 1
        timeline[e] -= 1
    for i in range(1, 360001):
        timeline[i] += timeline[i - 1]
        
    # ⑦, ⑧ 영상의 시작시간부터 timeline의 at(광고 재생시간)크기의 구간합이 최대가 되는 경우를 탐색
    # 다시 한번 누적 합을 구하는 전처리작업을 수행하여 구간 합을 더 쉽게 구할 수 있도록 한다.
    for i in range(1, 360001):
        timeline[i] += timeline[i - 1]
        
    # 최대 구간 합 maxw, 정답으로 반환할 시작시간 answer
    maxw, answer = timeline[at-1], 0
    
    # 영상의 시작시간부터 탐색 시작
    for s in range(1, pt - at+1):
        # 누적 합의 성질을 활용하여 구간 합을 구한다
        w = timeline[s+at-1] - timeline[s-1]
        
        # 만약 새로운 구간합이 최대 구간합 maxw보다 크다면 maxw와 answer 갱신
        if w > maxw:
            maxw, answer = w, s
            
    # 광고를 삽입할 최적의 시작시간을 문자열로 변환하여 반환
    answer = htos(answer)
    return answer


# 문자열 형태의 시간을 초단위의 정수로 변환
def stoh(times):
    h, m, s = map(int, times.split(':'))
    return h * 3600 + m * 60 + s


# 초단위의 정수를 문자열 형태의 시간으로 변환
def htos(sec):
    h = sec // 3600
    sec %= 3600
    m = sec // 60
    s = sec % 60
    return '%02d:%02d:%02d' % (h, m, s)

 

2) 투 포인터를 활용하여 최대 구간합을 구한 방식 (⑧)

def solution(play_time, adv_time, logs):
    # ① 광고의 재생시간이 영상의 재생시간과 같다면 영상의 첫 부분에 삽입하면 된다.
    if play_time == adv_time:
        return '00:00:00'
    
    # ② 영상 재생시간과 광고 재생시간을 초단위의 정수로 변환
    pt, at = stoh(play_time), stoh(adv_time)
    
    # ③ 시간별 재생횟수를 나타내기위한 타임라인 배열을 생성
    timeline = [0] * 360001
    
    # ④, ⑤ 로그를 분석하여 재생 시작시간과 종료시간을 구한 뒤
    # 누적 합을 활용하여 타임라인에 재생 횟수를 표기
    for log in logs:
        s, e = map(stoh, log.split('-'))
        timeline[s] += 1
        timeline[e] -= 1
    for i in range(1, 360001):
        timeline[i] += timeline[i - 1]

    # ⑦, ⑧ 영상의 시작시간부터 timeline의 at(광고 재생시간)크기의 구간합이 최대가 되는 경우를 탐색
    
    # 최대구간합 maxw, 정답으로 반환할 시작시간 answer
    maxw, answer = sum(timeline[:at]), 0
    
    # 초기 구간합 w = maxw
    w = maxw
    
    # 영상의 시작시간부터 탐색 시작
    for s in range(1, pt - at+1):
        # 투 포인터를 활용하여 구간합을 갱신해나가는 방식을 사용
        w = w - timeline[s-1] + timeline[s+at-1]
        
        # 만약 새로운 구간합이 최대 구간합 maxw보다 크다면 maxw와 answer 갱신
        if w > maxw:
            maxw, answer = w, s
            
    # 광고를 삽입할 최적의 시작시간을 문자열로 변환하여 반환
    answer = htos(answer)
    return answer


# 문자열 형태의 시간을 초단위의 정수로 변환
def stoh(times):
    h, m, s = map(int, times.split(':'))
    return h * 3600 + m * 60 + s


# 초단위의 정수를 문자열 형태의 시간으로 변환
def htos(sec):
    h = sec // 3600
    sec %= 3600
    m = sec // 60
    s = sec % 60
    return '%02d:%02d:%02d' % (h, m, s)

 

구간 합을 활용하여 효율적으로 문제를 해결해야하는 경우는 코딩테스트에서 꽤 자주 보인다.  잘 기억해두자.

+ Recent posts