한국정보올림피아드(정올) 2017년도 초등부 문제 풀이 포스팅입니다.


문제들은 백준저지에서 참고하고 풀이했습니다.

https://www.acmicpc.net/category/detail/1775


총 4문제로 이루어져 있는데, 제가 풀 때 당시에는 백준저지에는 3문제만 올라와있어서 3문제에 대한 코드만 올립니다.


https://www.acmicpc.net/problem/14697


2번, 방 배정하기 문제입니다. (백준저지 14697)


N값이 작으므로 3중 포문으로 O(N^3)으로 완전탐색으로 Naive하게 풀었습니다.


#include <bits/stdc++.h>
using namespace std;

int main() {
    int a, b, c, total;
    cin >> a >> b >> c >> total;
    
    int a_lim = total/a;
    int b_lim = total/b;
    int c_lim = total/c;
    for (int i=0; i <= c_lim; i++) {
        int cur = c * i;
        for (int j=0; j <= b_lim; j++) {
            int cur2 = cur + j*b;
            for (int k=0; k <= a_lim; k++) {
                int cur3 = cur2 + k*a;
                if (cur3 == total) {
                    cout << 1 << '\n';
                    return 0;
                }
            }
        }
    }
    cout << 0 << '\n';
	return 0;
}


https://www.acmicpc.net/problem/14863

3번, 서울에서 경산까지 문제입니다. (백준저지 14863)


DFS를 통한 완전탐색으로 풀되, 중복되어 풀리는 문제들(Overlapping-subproblems)는 Memoization 패턴을 이용하여 중복해서 풀리지 않도록 처리했습니다. 그리고 마지막 구간까지 도달하지 못하는 경우에 대해서 제외하도록 처리해야하는 것이 좀 까다로웠습니다.



#include <bits/stdc++.h>
using namespace std;

#define IMPOSSIBLE -987654321
typedef long long ll;
int dp[101][100001]; //[range][minute left]
int walk_min[101];
int walk_get[101];
int bicy_min[101];
int bicy_get[101];
int n, k;
int get(int segment, int minute_left) {
    if (segment >= n) return 0;
    if (minute_left <= 0) return IMPOSSIBLE;
    int& ret = dp[segment][minute_left];
    if (ret != -1) {
        return ret;
    };
    
    ret = IMPOSSIBLE;
    if (minute_left >= walk_min[segment] and get(segment+1, minute_left - walk_min[segment]) != IMPOSSIBLE) {
        ret = max(ret, get(segment+1, minute_left - walk_min[segment]) + walk_get[segment]);
    }
    if (minute_left >= bicy_min[segment] and get(segment+1, minute_left - bicy_min[segment]) != IMPOSSIBLE) {
        ret = max(ret, get(segment+1, minute_left - bicy_min[segment]) + bicy_get[segment]);
    }
    return ret;
    
}

int main() {

    scanf("%d %d", &n, &k);
    for (int i=0; i < n; i++) {
        scanf("%d %d %d %d", &walk_min[i], &walk_get[i], &bicy_min[i], &bicy_get[i]);    
    }
    memset(dp, -1, sizeof(dp));
    
    cout << get(0, k) << '\n';
	return 0;
}


https://www.acmicpc.net/problem/14864

4번, 줄서기 문제입니다. (백준저지 14864)


개인적으로 난이도가 꽤나 있었고, 완전 Nice한 해답은 아니라고 생각합니다.

일단 순서쌍에서 알아낼 수 있는 정보는 다음과 같습니다.

1. 나보다 뒤에 있는 놈에 대해서, 내가 그 놈보다 큰지 아닌지를 확인할 수 있다.

2. 나보다 앞에 있는 놈 중에 나보다 큰 놈이 몇 개인지 알 수 있다.    


물론 나보다 앞에 있는 놈 중에 큰놈이 몇개인지 뿐만 아니라 누군지까지 확인할 수 있지만, 일단은 저 두개를 이용해서 문제를 풀어보았습니다.

맨 뒤에서 부터 채워가면서 숫자를 넣을 것인데, 일단 집합 S를 다음과 같이 초기화 합니다. S = {1, 2, 3, ... , N}

맨 뒤에있는 숫자 입장에서는 나머지 모두가 나보다 앞에 있으므로, 나머지 수 중 자신보다 큰 수가 몇개인지 알 수 있습니다.

그러면 앞에있는 수 중 자신보다 큰 수의 개수를 k라고 하면, 집합 S에서 k번째 큰 수를 빼서 자신의 값에 집어넣습니다.


이런식으로 맨 앞까지 진행합니다.


이러면 일단 조건을 만족할만한 정답 셋을 하나 구할 수 있는데, Valid한 정답을 갖는다면 Unique한 정답을 가질 것입니다.

하지만 해당 인풋이 Invalid할 수도 있으니 한번 체크를 해야 합니다. (Invalid한 정답이라면 -1를 출력해야함)

이때 Strict하게 체크를 하려면 자신보다 뒤에 있는 숫자 중, 순서쌍에 해당된다면 자신보다 작은지, 순서쌍에 해당되지 않는다면 자신보다 큰지를 확인해야하는데, 둘다 체크하게 되면 TLE가 날 것입니다.   

특히나 순서쌍애 해당되지 않는 애들까지 체크를 하게 되면 O(N^2)이므로 꼼짝없이 TLE다. 하지만 순서쌍 개수는 100만개이므로, 순서쌍에 해당되는 경우만 체크할 경우 TLE는 안날 수 있습니다. 따라서 순서쌍에 해당되는 경우만 자신보다 작은 값을 갖는지만 체크하면 됩니다.


이 부분의 경우 -1이 정답인데 아니게 나올까봐 조마조마했는데 정답 판정이 났다. 아마 앞에서 만든 Answer Array가 주어진 순서쌍 조건이 맞는지만 확인해도빠진 조건이 있는 반례가 없다는게 논리적으로 맞아서 그런 것인지는 잘 모르겠습니다.

#include <bits/stdc++.h>
using namespace std;

#define NUM_STUDENT 100001 //학생수 최대
#define UNDEFINED 0 // 정답이 결정되지 않은 상태
set<int> larger_than_me[NUM_STUDENT], smaller_than_me[NUM_STUDENT];

int ans[NUM_STUDENT];
int n, m;
set<int> rest;
void fail() {
    cout << -1 << '\n';
    exit(EXIT_SUCCESS);
}
int main() {
    scanf("%d%d", &n, &m);
    
    //initialize rest set
    for (int i=1; i<=n;i++) {
        rest.insert(-i);
    }
    //initialize rest set
    
    if (n == 1) { //학생수 1명일때만 예외처리
        cout << 1 << '\n';
        return 0;
    }
    
    for (int i=0; i < m; i++) {
        int a, b;
        scanf("%d %d", &a, &b); // a > b라는 뜻
        smaller_than_me[a].insert(b); // a보다 작은 set에 b 추가.
        larger_than_me[b].insert(a);// b보다 큰 set에 a추가.
    }
    
    for (int cur = n; cur >= 1; cur--) {
        int larger_num = larger_than_me[cur].size();
        auto it = rest.begin();
        for (int i=0; i < larger_num; i++) {
            it++;
        }
        int kth_largest = -*it;
        ans[cur] = kth_largest;
        rest.erase(it);    
    }
    
    //check for validation - 주어진 조건은 맞는지 확인. 빠진 조건이 있는지는 안확인 ㅎㅎㅎ
    for (int cur = 1; cur < n; cur++) {
        int my_val = ans[cur];
        for (auto it : smaller_than_me[cur]) {
            int smaller_val = ans[it];
            if (smaller_val > my_val) {
                fail();
            }
        }
    }
    
    for (int i=1; i <= n; i++) {
        cout << ans[i] << ' ';
    }
    cout << '\n';
	return 0;
}


GDB는 일반 어플리케이션 디버거로서도 사용되지만, 특히나 시스템 해킹을 공부하는 사람들이 많이 사용하곤 한다. 자주 사용하는 명령어들을 한번 정리해보겠다.


1. 실행

바이너리 실행 및 디버거 Attach

gdb -q [파일명]


런타임 Attach

gdb -q -p [pid]


2. 메모리 검사(Examine/ x 명령어)


x/[개수][포맷][크기] [주소]


개수에는 숫자가 들어간다


포맷은 다음의 경우가 있다.

1) x - 16진수

2) d - 10진수

3) c - 문자

4) s - 스트링

5) i - 인스트럭션(어셈블리 명령어)


크기는 다음의 경우가 있다.

1) b - 바이트(1byte)

2) h - 하프워드(2byte)

3) w - 워드(4byte)

4) g - 자이언트(8byte)


주소는 다음과 같이 있다.

1) 0xabfdefg - 16진수로 주소값을 직접 사용

2) $eip - 레지스터가 가지고 있는 값의 메모리 주소를 검사. 레지스터 이름 앞에 $를 붙여야 한다.



3. 디버기(debugee) 프로세스의 pid 및 정보 알아내기

(gdb) info inferior


4. 브레이크 포인트 걸기


(gdb) b *0x808080 
(gdb) b *main 
(gdb) b *main+5


와 같이 다양하게 브레이크 포인트를 걸 수 있다.


5. 브레이크 포인트 걸린 위치 확인


(gdb) info b


6. 프로세스 실행

(gdb) run

혹은

(gdb) r


7. 다음 브레이크 포인트까지 계속 실행

(gdb) continue

줄여서

(gdb) cont


8. 한 인스트럭션만 실행

(gdb) next instruction

(gdb) ni


-----------------

syscall들

https://syscalls.kernelgrok.com/



컴퓨터를 사용함에 있어서 데이터 크기에 대한 단위들이 여러가지 있다. 그런 단위들에 대해 간략하게 알아보도록 한다.


- 비트(bit) 

https://en.wikipedia.org/wiki/Bit

비트는 Binary Digit의 약자이다. 직역하면 이진 숫자가 된다. 0아니면 1밖에 없으므로 이진(binary)가 맞으며, 숫자를 나타내는 것이 맞다. 이진법으로 나타낸 수의 한 자리에 차지하고 있는 숫자 하나를 의미한다고 봐도 의미가 맞다.


이 bit라는 용어는샤넌이라고 하는 사람이 1948년에 낸 논문에서 처음 사용했다고 한다. 저 샤넌이라고 하는 사람은 "정보공학의 아버지"라고 불린다고 한다. 수학자이면서, 전자전기공학자, 암호학자였고 bit라는 개념은 최초로 정보의 크기를 정의했다는 점에서 의미가 있다고 한다.

1946년에 애니악이 제작되었으므로, 컴퓨터 공학의 입장에서는 최초의 컴퓨터가 생긴지 얼마 안되었을 때 나타난 개념이다.


지금의 컴퓨터는 정보를 처리하는 수단으로서 주도적인 역할을 하고 있으니, 이때 정보의 크기를 규정할 수 있다는 것은 큰 의미가 있다고 볼 수 있다.


- 니블(nibble)

https://en.wikipedia.org/wiki/Nibble

니블은 4bit를 나타낸다. 하프 바이트(half-byte) 혹은 테트라드(tetrade)나 세미-옥텟(semi-octet), 쿼드비트(quadbit)나 쿼텟(quartet)이라고 불리기도 한다. 그리고 4비트는 10진수로 0부터 15까지의 수를 나타낼 수 있으므로 16진수(hexa-decimal)로 나타낸 수의 한 글자(digit)이라고 볼 수 있다. 따라서 hex-digit라고도 불린다. 하지만 요즘은 이러한 다양한 부르는 방법들을 잘 쓰지 않고, 궂이 언급한다면 hex code의 한 글자로서 간접적으로 불린다.

이러한 4비트에 따로 이름을 두어서 사용하는 이유는 10진수 한 자리 수(0~9)를 나타낼 수 있는 최소 정보단위가 4비트이기 때문이다.


- 바이트(byte)

https://en.wikipedia.org/wiki/Byte

원래의 의미는 글자 1글자를 나타내기 위한 용량을 뜻하게 되었는데, ASCII코드로 영어 대,소문자 및 일부 특수문자들을 다 표현할 수 있으므로, ASCII의 7bit에 패리티 비트 1bit를 붙여서 8bit로 영어 문자들을 다 표현할 수 있다. 이로써 1바이트가 8bit로 관례적으로 굳어지게 된다. 일반적으로 1바이트라고 하면 8비트를 뜻하게 된다.



아마 알고리즘 PS(Problem Solving) 분야를 공부하시는 분이라면 한번쯤은 CP(Competitive Programming)에도 참여해보셨을 것입니다. PS가 알려진 알고리즘을 이용해서 프로그래밍 문제를 푸는 것이라고 한다면, CP는 이러한 형태의 문제를 같은 시간대에 여러명이 참여해서 제한시간내에 문제를 풀고 점수를 획득하여 랭킹을 매긴 뒤 Rating을 얻는 방식이라고 할 수 있습니다.


이러한 CP에는 세계적으로 가장 유명한 대학생 대상 프로그래밍 대회인 ACM-ICPC를 비롯하여 온라인에서 자주 이루어지는 Codeforce Contest, AtCoder Contest와 Topcoder Single Round Match 등이 있습니다. 이 외에도 저는 참여해보지 않았지만, Facebook Hacker Cup이나 Hacker Rank같은 것도 비슷한 종류의 대회라고 생각합니다.


어쨋든간 저는 대부분 Codeforce Contest에 참여하곤 했는데 이번에 기회가 되어서 Topcoder SRM에 참여해보려고 했습니다. 인터넷에서 SRM 참여 관련 자료들이 꽤나 있었는데, 옛날 자료이기도 하고 Topcoder 플랫폼 자체가 많이 변해서 자료들과는 조금 다른 점들과 해맨점이 있었습니다.


Codeforce의 경우는 사이트 접속시 직관적인 UI로 쉽게 Contest에 참여할 수 있었는데요 Codeforce의 경우는 알고리즘 문제 해결이 사이트의 주요 목적이다보니 컨테스트 참여와 문제풀이와 같은 UI는 쉽게 익숙해질 수 있었습니다. 하지만 Topcoder의 경우는 SRM 외에도 다양한 많은 기능들을 제공하고 있다 보니 SRM 참여와 SRM 시작 시기를 아는 것이 조금 불편했습니다.


이제 SRM의 참여 방법과 관련된 이야기를 해보려고 합니다.


일단 첫번째로는 Topcoder SRM의 개최 시기를 알아야겠지요? 다음 링크로 이동해봅니다.

https://www.topcoder.com/community/events/


그러면 위와 같은 켈린더를 확인할 수 있는데요, 여기서 하단에 시간대를 보면 "동부 표준시"라는 것을 알 수 있습니다. 이는 구글링 해 보면 
 UTC-05시, 섬머 타임 시에는 UTC-04라는 것을 알 수 있습니다. 한국 표준 시간인 KST의 경우 UTC+09인 것을 감안하면 5+9=14 즉 14시간 정도 차이가 나는 것으로 알 수 있습니다. 즉 구글 캘린더에 나타난 시간에서 14시간을 더해주어야지 한국 시간인 것입니다. 이미지에 나타난 SRM 727의 경우 1월 10일 오후 9시이지만, 한국 시간으로는 1월 11일 오전 11시에 시작했습니다.


이러한 불편함은 다음 링크에 있는 알고스팟 캘린더를 확인하면 간편합니다.

https://algospot.com/calendar/

알고스팟 캘린더의 하단을 보면, 시간 기준이 "서울"로 되어있습니다. 앞에 나와있는 [CF]는 Codeforce의 약자이며, [TC]는 탑코더의 약자입니다. 알고스팟 캘린더는 프로그래밍 콘테스트의 일정이 모여있으므로 훨씬 편리하게 확인할 수 있습니다.


또는 백준저지 캘린더를 확인해도 됩니다. 백준 저지 캘린더에 훨씬 많은 일정들이 빽빽하게 들어있습니다.

https://www.acmicpc.net/calendar




이제 탑코더 사이트로 이동해보도록 하겠습니다.

https://www.topcoder.com


첫 화면 페이지부터 여타 다른 OJ나 CP용 홈페이지와는 조금 다른, 기업용 페이지 같은 모습을 띄고 있습니다. 

일단 화면 중앙의 "I want to join Topcoder" 를 눌러서 회원가입을 했다고 가정하겠습니다.


우측 상단의 Login 버튼을 누르면 중앙에 보이는 것 처럼 두개의 창이 뜨게 됩니다. 이 중 우리의 목적은 SRM을 통한 CP이므로, It's time to compete and win에 맞는 Community log in을 누릅니다.


로그인을 하게 되면 다음과 같은 페이지로 이동하게 되는데, 상단 위젯을 통해서 Competitive Programming을 누릅니다.


그러면 다음과 같은 화면이 나타나게 됩니다. 우측에 나타난 캘린더에서 빨간색으로 표시된 날짜에는 SRM이나 MM(Marathon Match)와 같은 이벤트 등록된 날짜이며, 실제 발생할 시간이 되기 전에는 미리 빨간색으로 나타나지 않는 것으로 보입니다. 따라서 위에서 확인한 구글 캘린더에 있는 아직 시행되지 않은 Round가 보이지 않는다고 해서 불안해 할 필요는 없습니다.


왼쪽부분에는 SRM의 각 Phase 별 시작 시간과 종료 시간에 대해서 설명이 되어 있습니다. 

SRM 라운드 스케쥴과 관련된 설명은 다음 링크에 있습니다.

https://www.topcoder.com/member-onboarding/competing-in-an-algorithm-match-srm/

SRM 등록의 경우 SRM 시작 3시간 전 부터 시작 5분 전까지 가능하다고 쓰여있지만 최근에는 4시간 전 부터 가능한 경우가 많다고 합니다.


다른 포스팅들을 보면 java로 작성된 클라이언트를 다운로드해서 참여해야한다고 나와있지만, 최근에는 업데이트가 되었는지 별도의 프로그램 설치 없이 웹 인터페이스만으로도 대회 참여가 가능합니다. 온라인에서 컴파일 및 TestCase 실행을 해 볼 수 있지만, 반응이 좀 느리고 서버가 약간 불안정했습니다.


대회가 끝나면 다음 링크에 풀이인 Editorial이 올라오는 것으로 보이며, 비공식적으로 코드포스 블로그에 올라오는 경우도 있는것으로 보입니다.

https://www.topcoder.com/community/data-science/data-science-editorials/


혹시 최근에 탑코더 SRM에 입문하실 의향이 있는 분들이 있다면 제 글을 보고 도움이 되기를 바랍니다.



* Topcoder SRM 관련해서 더 읽어볼만한 글들

https://www.acmicpc.net/blog/view/2

https://algospot.com/wiki/read/TopCoder_%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC

본 포스팅은 다음 링크의 내용을 통해 간추려서 번역한 포스팅입니다.

https://www.digitalocean.com/community/tutorials/how-to-install-linux-apache-mysql-php-lamp-stack-on-centos-7


LAMP 스택은 리눅스 서버에서 간단하게 동적 웹 앱을 호스팅 할 수 있는 간단한 스택 중 하나이다. LAMP는 Linux, Apache, MySQL(혹은 Maria DB), PHP의 머릿글자를 짠 오픈소스 소프트웨어 스택이다.


이제 CentOS 7에 LAMP 스택을 구축해보도록 하겠다.


<선행사항>

일단 Root유저가 아닌 유저가 시스템에 추가되어 있어야 한다.



<아파치 설치>


다음 명령어로 아파치 서버를 설치할 수 있다.


sudo yum install httpd




다음 명령어로 아파치 서버를 실행시킬 수 있다.

sudo systemctl start httpd.service



아파치 서버 실행 이후 웹 브라우저를 켜서 주소창에 localhost라고 입력하면 아파치 테스트 페이지를 확인할 수 있다.

테스트 페이지가 나타난다면 아파치 서버가 정상적으로 설치 및 실행된 것이다.


다음 명령어를 입력하면, 컴퓨터가 부팅될 때 자동적으로 아파치 서버가 실행되도록 설정할 수 있다.


sudo systemctl enable httpd.service




<서버 아이피를 확인하는 방법>

다음 명령어를 입력하면 현재 장비에서 연결된 네트워크 인터페이스들이 나타난다.



ip addr show


그 중 해당되는 인터페이스의 inet 항목의 첫번째 나타나는 4개의 10진수 숫자가 IP주소이다. 물론 이 주소는 IPv4(IP 버전4)의 주소이며, IPv6(IP 버전6)의 주소의 경우는 inet6라고 나타나는 필드의 바로 우측에 나타난다.


<MySQL혹은 MariaDB 설치>

이제 웹 서버에서 사용할 DBMS를 설치할 것이다. MySQL과 MariaDB는 서로 상호호환 되므로 아무거나 깔아도 무방하다. 이번 포스팅에서는 MariaDB를 설치 해 보도록 하겠다.




sudo yum install mariadb-server mariadb



설치가 끝나면 MariaDB를 다음 명령어를 통해서 서비스 실행을 해 보도록 하겠다. DBMS는 서버와 클라이언트로 이루어져 있는데, 일단 서버를 구동해야 하며, 서버가 구동 중 인 경우 클라이언트가 서버에 접속해서 원하는 동작들을 수행할 수 있다. 다음 명령어는 서버를 구동하는 명령어이다.



sudo systemctl start mariadb



MariaDB를 설치한 뒤, 기본적으로 설정되어 있는 DBMS 설정 중 보안적으로 위험한 것들을 대화형 모드로 하나하나 체크하기 위해 다음 명령어를 실행한다.



sudo mysql_secure_installation


대화형 모드로 MariaDB의 Root 패스워드 변경, 익명 유저의 제거, 원격에서 Root 권한 로그인 불가 설정, 테스트 Database와 그 접근 권한 제거, 권한 테이블 반영과 같은 기본 보안 설정들을 처리해주게 된다.


또한 MariaDB도 부팅 시 자동으로 서비스가 동작하게 하기 위해 다음 명령어를 입력한다.



sudo systemctl enable mariadb.service



<PHP 설치>

PHP의 기능을 강화하기 위해서, 선택적으로 PHP의 추가 모듈들을 설치할 수 있습니다. 이러한 추가적인 PHP 모듈들과 라이브러리 리스트를 확인하기 위해 다음의 명령어를 입력할 수 있다.



yum search php-


나타나는 패키지 중, 필요한 패키지들을 설치한다. 패키지 설치는 다음과 같은 명령어를 사용하면 된다.

다음 명령어는 php-fpm 패키지, pacakge1 라는 이름을 갖은 패키지, package2라는 이름을 갖는 패키지를 설치하는 명령어이다.


sudo yum install php php-common php-mysql php-devel



<PHP의 동작 확인>


PHP가 아파치 서버에서 잘 동작하는지 확인하기 위해 간단한 PHP 코드를 웹을 통해서 실행해보도록 한다.

CentOS 7에서 아파치 기본 디렉토리에 php 스크립트 파일을 생성해 보도록 한다.

다음 명령어로 텍스트 편집기를 실행한다.


sudo vi /var/www/html/info.php


그리고 해당 파일에 다음과 같이 입력한다.



<?php phpinfo(); ?>


그리고 파일을 저장한다.


만약 방화벽을 구동중이라면 다음의 명령어로 HTTP와 HTTPS 트래픽을 허용해주어야 한다.


sudo firewall-cmd --permanent --zone=public --add-service=http sudo firewall-cmd --permanent --zone=public --add-service=https sudo firewall-cmd --reload


그리고 웹 브라우저에서 다음 URL을 입력하여 접속하여 php의 정보가 잘 출력되는지 확인해본다.


http://localhost/info.php

혹은

http://127.0.0.1/info.php


만약 php 잘 동작하지 않는다면 다음 명령어로 아파치 서버를 재시작 해본다.


sudo systemctl restart httpd.service




이클립스로 스프링 프레임워크 개발을 할 시 이클립스 환경 구축은 크게 두가지 방법이 있습니다.


1) 이클립스를 설치 한 후 STS를 별도로 설치

2) STS플러그인이 통합된 이클립스를 설치.



1번 방법의 설치는 이클립스를 설치한 뒤 다음 메뉴를 선택합니다.

이후 STS를 검색하여 설치하면 됩니다.

Install을 누릅니다.

이후 Confirm을 누릅니다.

라이센스의 동의를 체크한 뒤, Finish를 누르면 됩니다.


설치가 다 되면, 이클립스를 재시작할 것인지를 물어보는 대화상자가 나타납니다. 이 때 Yes를 눌러서 재시작합니다.


기존 이클립스에 STS를 설치한 경우 스프링 개발환경 뿐만 아니라 다른 개발환경도 지원하는 이클립스이므로, 스프링의 관점(Perspective)로 바꾸어주어야 합니다. 다음과 같은 과정을 통해서 스프링의 관점으로 변경합니다.


Windows - Perspective - Open Perspective - Others를 누릅니다. 만약 해당 엔트리 중에 Spring이 있다면 Spring을 바로 눌러주어도 상관없습니다.


Spring을 선택한 후 Open을 눌러주시면 됩니다.


이후에 다시 변경할 일이 생긴다면 해당 위치에 있는 Spring Perspective 아이콘을 눌러주시면 됩니다.



2번 방법의 설치는 다음 링크로 진입한 뒤, Download STS 버튼을 누르면 됩니다.

https://spring.io/tools/sts

해당 패키지를 다운로드 받은 뒤, 압축을 해제하고 실행하면 이미 STS가 설치되어있는 이클립스가 실행됩니다. 스프링을 개발하기 위해서 이클립스를 설치한다면 2번 방법을 이용하는 것이 훨씬 간단합니다.


만약 이미 이클립스를 설치하고 사용하고 있다면 조금 번거롭더라도 1번 방법을 사용하면 이클립스로 다양한 개발을 진행할 수 있습니다.

2017년 12월 22일 기준, 마이크로소프트에서 제공하는 아래 링크에서 Windows 10 부팅 USB 제작 툴을 구할 수 있습니다. Windows 10의 부팅 USB와 해당 iso 파일은 무료로 배포되지만, 해당 운영체제에 사용되는 라이센스 키, 과거 CD로 설치하던 시절에는 CD-key라고 불리기도 했고, Product Key라고도 불리는 키는 돈을 주고 구매를 하거나 해야 합니다. 간혹 노트북을 구매 시, 윈도우 운영체제가 이미 설치되어 있는 경우, 노트북의 겉 면을 잘 찾아보면 윈도우 운영체제의 Product Key가 표시되어 있거나, 별도의 등록 절차를 통해서 키 인증을 받을 수 있는 경우가 있으므로 해당 사항은 좀더 찾아보시면 되겠습니다.


어쨋든 키를 가지고 있다고 가정하고 Windows 10 부팅 USB를 만들어 보도록 하겠습니다.


그리고 내용이 지워져도 상관없는 8GB 이상의 용량을 가진 USB 플래시 메모리도 필요합니다.



https://www.microsoft.com/ko-kr/software-download/windows10


위와 같은 웹 페이지가 나타나게 되는데, '지금 도구 다운로드' 버튼을 누르면 MediaCreationTool.exe라는 파일을 받을 수 있습니다. 해당 툴을 실행시켜보도록 하겠습니다.


당연 조건을 동의해주어야지 사용이 가능합니다. 동의를 누릅니다.

부팅 USB를 만드는 것이므로 두번째 라디오 박스를 선택합니다.

설정에 맞게 선택합니다. 에디션은 Windows 10만 가능하며, 아키텍쳐의 경우 CPU호환성을 확인해 보아야 하는데, 현재 대한민국 기준으로 64비트가 호환되지 않는 CPU는 거의 없으므로 저는 그대로 갑니다.

USB 플레시 메모리에 윈도우 부팅 이미지를 올릴 것이므로 첫번째 라디오 박스를 선택합니다.

지금 부착되어 있는 외장 스토리지에 맞게 선택합니다. 저 같은 경우에는 E드라이브에 외장 하드디스크가 있고, F드라이브에 USB 플래시 메모리가 있기에, F드라이브를 선택합니다.

시간이 다소 걸립니다. 침착하게 기다리도록 하겠습니다.

완료된 모습입니다.

안전하게 USB를 제거한 뒤 물리적으로 분리하도록 하겠습니다.


이제 이 USB를 이용하여 윈도우10을 원하는 기기에 설치하면 됩니다.

https://www.acmicpc.net/problem/11279


백준저지 11279번 최대 힙 문제입니다.


파이썬이나 C++ STL에서 제공해주는 heap, priority_queue 자료구조를 사용하면 간단하게 풀 수 있지만, 최대힙을 직접 구현하면서 해당 자료구조에 대하여 한번 공부해도록 하겠습니다.


힙이라는 자료구조는 일단 ADT(Abstract Data Type)이기 때문에 직접적으로 메모리상에 어떠한 구조로 나열되든간에, 설계된 대로만 동작하면 맞습니다.

Complete Binary Tree라는 자료구조 특성상 일반 트리 처럼 포인터로 구현을 하기 보다는 배열로 구현하는 것이 편합니다.

저도 이번에 배열로 구현을 할 예정이며 C++ STL에서 제공하는 vector 컨테이너를 사용하겠습니다.


배열로 구현한 Binary Tree는 각 노드 번호 i에 대하여 Left child는 i*2, Right child는 i*2+1의 번호를 갖습니다. 그러기 위해서는 루트노드의 번호를 1부터 새야 하므로 data[0]에는 더미노드를 하나 넣어서 1부터 새도록 구현했습니다.


최대 힙은 두개의 연산을 제공하는데, 새로운 데이터를 넣는 연산과, 힙에서 최대값을 빼내는 연산입니다.

최대 힙의 구조상 부모 노드는 항상 자식 노드보다 값이 크므로, 힙에서 값을 빼내는 연산은 항상 루트 노드의 값을 빼내게 됩니다.

각각의 연산을 push와 pop이라고 명명하겠습니다.


push 연산은 다음과 같은 절차로 이루어집니다.

1. Complete binary tree가 되도록, 마지막 위치에 넣는 값을 집어넣습니다.

2.  직속 부모 노드와 값을 비교하여 자신의 값이 직속 부모 노드 보다 값이 작을 때 까지 부모 노드와 위치를 바꿉니다.


pop 연산은 다음과 같은 절차로 이루어집니다.

1. 리턴할 루트 노드의 값을 따로 빼놓습니다. 그리고 가장 마지막 노드의 값을 루트 노드의 위치로 이동시킵니다.

2. 이동된 노드가 자신의 양쪽 자식 노드와 비교해서 둘 중에 자신보다 큰 값을 가진 노드가 있다면 그 중 가장 큰 노드를 자신의 위치와 바꿉니다. 자신이 모든 직속 자식들보다 값이 클 때 까지 반복합니다.

#include <iostream>

#include <cstdio> #include <vector> using namespace std; #define MAX_SIZE 100001 class max_heap { private: vector<int> data; public: max_heap() { this->data.push_back(-1); // index 0 means dummy data } max_heap(int size) { this->data.reserve(size); this->data.push_back(-1); //index 0 is dummy data } int pop() { if (this->data.size() <= 1) { //heap is empty return 0; } int current = 1; int left_child = 2; int right_child = 3; int ret = this->data[current]; int last = this->data.size() - 1; swap(this->data[current], this->data[last]); this->data.pop_back(); while (this->data.size() > left_child) { //when left child exist int larger_index = left_child; if (this->data.size() > right_child) { //when right child exist if (this->data[right_child] > this->data[left_child]) { larger_index = right_child; } } if (this->data[larger_index] > this->data[current]) { swap(this->data[current], this->data[larger_index]); current = larger_index; left_child = current * 2; right_child = left_child + 1; } else { break; } } return ret; } bool push(int val) { this->data.push_back(val); int current = data.size() - 1; int parent = current/2; while (parent > 0 && this->data[current] > this->data[parent]) { swap(this->data[current], this->data[parent]); current = parent; parent = current/2; } } }; int main() { max_heap heap(MAX_SIZE); int n; cin >> n; for (int i=0; i < n; i++) { int tmp; cin >> tmp; if (tmp > 0) { heap.push(tmp); } else { cout << heap.pop() << endl; } } return 0; }


https://www.acmicpc.net/problem/1002


백준저지 1002번 터렛 문제이다.


어떤 지점에서 길이가 같은 점들의 집합이 원이므로, 원 두개를 그린 뒤 그 두개의 원의 교점의 개수를 리턴하면 된다.


이는 두 원의 반지름과, 두 원의 중점 사이의 거리와의 관계를 따져서 값을 출력하면 된다.


두 원의 반지름을 r1, r2 (r1 <= r2)이라고 하고, 중점 사이의 거리를 d라고 하자.


소스코드에서는 floating point의 부정확함을 고려하여서 실제 나타난 거리의 제곱수인 정수를 이용하여 비교를 하였다. 따라서 sqrt(Square root)와 같은 함수는 따로 사용할 필요가 없다.


다음 케이스들을 고려해주면 된다.


1. 원이 두 점에서 만나는 경우

r2 - r1 < d < r1 + r2


2. 두 원이 외접하는 경우 (한점에서 만난다)

d = r1 + r2


3. 두 원이 내접하는 경우 (한점에서 만난다)

d = r2 - r1

and

d != 0


4. 하나의 원이 다른 원을 포함하는 경우 (만나지 않는다)

d < r2 - r1


5. 두 원이 멀리 떨어져 만나지 않는 경우

d > r1 + r2


6. 두 원이 일치하는 경우 (무수히 많은 점에서 만난다)

d = 0

r1 = r2

 

#include <iostream>

using namespace std;

typedef long long ll;

int main() {
    int t;
    cin >> t;
    while(t--) {
        ll x1, y1, r1, x2, y2, r2;
        cin >> x1 >> y1 >> r1 >>x2 >>y2 >> r2;
        ll dx = x1-x2;
        ll dy = y1-y2;

        if (r1 > r2) swap(r1, r2);
        ll add = r1 + r2;
        add = add * add;
        ll sub = r2 - r1;
        sub = sub * sub;
        ll d = dx*dx + dy*dy;
        
        if (d < add && d > sub) {
            cout << 2;
        } else if (d == add || (d == sub && d != 0) ) {
            cout << 1;
        } else if (d < sub || d > add) {
            cout << 0;
        } else if (d ==0) {
            if (r1 == r2)
                cout << -1;
            else
                cout << 0;
        }
        cout << '\n';
    }
    return 0;
    
}


팰린드롬인 부분수열 개수 구하기(Counting All Palindromic Subsequences) 알고리즘에 대하여 포스팅을 해 보도록 하겠습니다.

일단 각각의 정의에 대해서 한번 설명을 간단하게 해 보겠습니다.


팰린드롬(Palindrome)은 앞에서부터 읽으나, 뒤에서부터 읽으나 같은 내용인 단어를 뜻합니다. 예를들어 '토마토'라던지 '바밤바', 'level', 'racecar' 등등이 있겠죠.


부분수열(Subsequence)은 원래 수열이나 배열에서 순서는 그대로 두고 일부 원소를 제거한 수열을 뜻합니다. 예를들어 abcdefg 라는 수열(Sequence)가 있다고 할 떄 abc와 bcd 와 같은 녀석들도 부분수열이 되지만, adg와 같은 녀석도 부분수열이 됩니다. 하지만 순서가 바뀌는 cba 같은 녀석은 부분 수열이 안됩니다.


그러면 이제 어떠한 문자열(String)에 대하여 팰린드롬인 부분수열(Palindromic subsequence)의 개수를 세 보도록 하겠습니다. 


일단은 부분수열이 중복이라도, 위치가 다르면 다른 것으로 세는 경우부터 따져보도록 하겠습니다.

즉, 예시로 'aab'의 팰린드롬 부분수열은 { 'a', 'a', 'b', 'aa' }가 되며, 'a'가 2개이지만 문자열의 첫번째 원소와 두번째 원소로 각각 샌 경우입니다.


일단 이 문제에 대해서 풀이법을 스스로 한번 생각해보는 것을 추천한다고 합니다.

다음 GeeksforGeeks 포탈에서 문제를 한번 풀어볼 수 있습니다. 로그인이 필요하긴 합니다만 별도의 개발환경 설정 없이 문제를 풀어볼 수 있습니다.

http://practice.geeksforgeeks.org/problems/count-palindromic-subsequences/1




이 문제 답안의 특징을 살펴보면, 일단 길이가 1인 문자열에 대해서는 팰린드롬 부분수열이 무조건 1개입니다. 그 보다 긴 경우를 고려를 해야 하는데 부분문제로 나누어서 풀이할 수 있습니다. 즉 다이나믹 프로그래밍으로 해결을 할 수 있지요. 아래와 같은 recurrence가 나타납니다.


classic dynamic programming problem, which can be solved using the following recurrence:

  • Let P(L, R) be the number of palindromic subsequences in S[L...R], then
  • P(L, R) = \begin{cases} 1 &\text{if } L = R \\P(L + 1, R) + P(L, R - 1) - P(L +1, R - 1) &\text{if } S(L) \neq S(R) \\ P(L + 1, R) + P(L, R - 1) + 1 &\text{if } S(L) = S(R)\end{cases}


< 출처: csacademy.com >


P(L,R)은 부분문자열 S[L...R]에서 나타나는 팰린드롬 부분수열이고, L과 R은 부분문자열의 시작과 끝 인덱스 번호입니다. L=R인 경우는 길이가 1인 부분 문자열입니다.


S(L) != S(R)인 경우는 왼쪽에서 범위가 1만큼 줄어든 P(L+1, R)과 오른쪽에서 범위가 1만큼 줄어든 P(L, R-1)의 개수를 더한 뒤, 중복으로 더해진 P(L+1, R-1)를 빼 줍니다. P(L+1, R)와 P(L, R-1) 둘 다 P(L+1, R-1)의 범위에서 나타난 정답값을 중복으로 포함하기 때문에 빼 준 것이죠.


S(L) = S(R)의 경우에는 위 경우와 달리 중복으로 더해준 P(L+1, R-1)을 빼지 않고 또 1을 더해줍니다. 

 { P(L+1, R-1)에 해당하는  개수 } = { P(L+1, R-1) 부분수열의 양 끝에 S(L)과 S(R)을 붙인 새로운 팰린드롬 부분수열 개수 }


이기 때문이죠. 그리고 추가적으로 1을 더한 값은 S(L)과 S(R)로만 이루어진 길이가 2인 부분수열의 개수입니다.


이렇게 재귀적인 방법으로 문제를 풀 수 있으며, 재귀 트리를 그려보면 겹치는 부분문제들이 있으므로 이를 메모이제이션(memoization)한 다이나믹 프로그래밍을 이용하여 효율적으로 문제를 해결할 수 있습니다.


Top-Down 방식으로 작성한 DP 솔루션 코드입니다.

/* Please note that it's Function problem i.e. you need to write your solution in the form of Function(s) only. Driver Code to call/invoke your function would be added by GfG's Online Judge.*/ int** cps; int get(int start, int end, string& str); /*You are required to complete below method */ int countPS(string str) { int ans; int len = str.length(); cps = (int**) calloc(sizeof(int*) , (len+1)); for (int i=0; i < len+1; i++) { cps[i] = (int*) calloc(sizeof(int), (len+1)); } ans = get(0, len-1, str); //free allocated memory for(int i=0; i <len+1; i++) { free(cps[i]); } free(cps); return ans; //Your code here } int get(int start, int end, string& str) { if (start > end) return 0; if (start == end) return 1; if (cps[start][end]) return cps[start][end]; if (str[start] == str[end]) { cps[start][end] = get(start, end-1, str) + get(start+1, end, str) + 1; } else { cps[start][end] = get(start, end-1, str) + get(start+1, end, str) - get(start+1, end-1, str); } return cps[start][end]; }



그리고 이제 중복을 허용하지 않는 팰린드롬 부분수열의 개수를 세는 경우를 따져보도록 하겠습니다.

앞선  예시에서 'aab'의 팰린드롬 부분수열은 중복을 허용하는 경우{ 'a', 'a', 'b', 'aa' }가 되며, 중복을 허용하지 않는 경우는 { 'a', 'b', 'aa' } 가 됩니다.

중복을 허용하지 않는 경우 중복을 허용하는 경우 보다 조금 더 까다로운데, dynamic programming의 점화식의 인자가 2개에서 3개로 늘어나게 됩니다. 3번째 인자는 가장 바깥에 감싸고 있는 알파뱃을 나타내는 시그마입니다.






  • Let DP(L, R, \sigma) be the number of distinct palindromic subsequences in S[L..R] which are bordered with \sigma.
  • DP(L, R, \sigma) = \begin{cases} 0 &\text{if } L > R \\ 1 &\text{if } L = R \ \text{and} \ S(L) = \sigma \\DP(L + 1, R, \sigma) + DP(L, R - 1, \sigma) - DP(L +1, R - 1, \sigma) &\text{if } S(L) \neq \sigma \ \text{or} \ S(R) \neq \sigma \\ 2 + \sum_{\theta \in \{a, b, .., z\}} DP(L + 1, R - 1, \theta) &\text{if } S(L) = S(R) = \sigma \end{cases}

S(L)=S(R)=sigma 의 경우는 DP(L+1, R-1, *)를 모두 더하는데, 원래 있던 각각의 부분수열들의 가장 바깥에 σ 글자로 패딩을 입힌 글자들을 셉니다. 그리고 더해주는 2의 경우는 { 'σ' , 'σσ' }의 경우가 됩니다. 기존의 팰린드롬 문자열들은 σ 패딩을 입혔기 때문에 모두 길이가 3 이상이므로, 중복이 될 일은 없습니다.


그러면 약간 헷갈릴 수 있는 부분이, 글자가 σ가 아닌 길이가 1인 팰린드롬 문자열들의 경우는 어떻게 하냐고 할 수 있는데, 해당 항목들은 DP(L, R, σ)가 아닌 닌 곳에서 세고있기에 상관이 없습니다. 

예를 들어서 'aba' 문자열에 대하여 팰린드롬 부분수열은 다음과 같습니다.

{ a, b, aa, aba }


위 Recurrence를 적용해보면

DP(1, 3, a) = DP(2, 2, *) + 2

DP(2, 2, *) = { b }

입니다. 따라서 DP(2, 2, *)에 a로 패딩을 입힌 새로운 팰린드롬 부분수열 { aba } 과, +2에 해당하는 { a, aa }를 합집합 한 결과인 { a, aa, aba }가 DP(1, 3, a)에 해당되고, { b }는 DP(1, 3, b)에 포함됩니다.


따라서 DP(1,3,a) = { a, aa, aba } 이고 / DP(1,3,b) = { b } 이됩니다. 

따라서 글자가 σ가 아닌 길이가 1인 팰린드롬 문자열들의 경우는 DP(L, R, σ)에서 세고 있지 않기에 위의 의문은 해결이 되었습니다.


제가 해당 솔루션을 보고 이해가 가지 않은 부분들을 고심하면서 해결한 내용을 블로그에 포스팅 해 보았는데, 혹시나 궁금하신점이나 의견있으시면 댓글로 달아주시면 내용에 반영하도록 하겠습니다.


[참고]

http://www.geeksforgeeks.org/count-palindromic-subsequence-given-string/

https://csacademy.com/contest/round-57/task/distinct-palindromes/solution/


+ Recent posts