예전에 '온라인저지 구축기' 라는 글을 올린 적이 있었는데, HUSTOJ가 버전업이 계속 되었고, 설치방법에 대한 문의가 있어 다시 적어봅니다.


설치 OS : Ubuntu 16.04.2 LTS

HUSTOJ repo : https://github.com/zhblue/hustoj (2018년 2월 28일 버전)



1. git을 설치한다.

sudo apt-get install git 



2. git clone으로 저장소에서 파일을 복사해온다.

아래 명령어는 항상 최신 버전을 받아오기 때문에, 이전 버전이 필요하다면 github에서 이전 커밋을 찾아보아야 한다.

git clone https://github.com/zhblue/hustoj



3. 받은 파일의 trunk/install 경로에서 install-ubuntu16+.sh 파일을 실행한다.

여기서는 문제 발생을 위해 기본 설정을 따를 것이므로, mysql이나 nginx가 이미 설정되어 있는 환경에서는 충돌이 발생할 수도 있다. (위의 repo에서 Docker 이미지도 제공하고 있으므로, 도커 사용에 익숙하다면 해당 이미지를 이용하여 쉽게 설치할 수 있다.)

sudo bash ./install-ubuntu16+.sh



4. 설치 도중에 mysql을 설치하고, 비밀번호를 설정하는 창이 나온다. 비밀번호를 입력하여 설치를 계속한다.


5. 수 분을 기다리면 설치가 완료된다. 경우에 따라 경고 메시지가 뜰 수 있는데, 심각한 내용이 아니면 일단 넘어간다. judge 라는 리눅스 계정이 생성되고, /home/judge 라는 홈 디렉터리가 만들어진다. 이 경로에 들어가서 파일이 제대로 설치되있는지 확인한다.



6. ps -ef | grep judged 를 실행하여 judged 데몬이 제대로 실행되고 있는지 확인한다.



7. 브라우저를 열고 127.0.0.1 (원격 접속중이라면, 해당 서버의 IP) 으로 접속하여 페이지가 잘 나오는지 확인한다.



8. /home/judge/src/web/include/db_info.inc.php 을 수퍼유저 권한으로 열어서(sudo) 언어를 수정한다. 언어 설정과 관련되어 있는 'cn'(중국어)이 2개 있다 이것을 en으로 바꿔준다.

zh-CN 라고 되어있는 것은 en-US로 수정한다.



9. 이제 관리자 계정을 생성해야 하는데, 기본 관리자 계정이 없어서 아이디를 만들고 DB를 수정해야 한다. 상단의 Login 메뉴에서 회원가입을 한다.



10. mysql -u root -p로 mysql 클라이언트를 열고 4번에서 지정한 비밀번호를 입력한다.



11. use jol; 을 입력하여 데이터베이스를 선택한다.



12. INSERT INTO privilege(user_id, rightstr) VALUES('생성한 아이디', 'administrator'); 쿼리를 입력하여 유저권한을 지정한다.



13. exit를 쳐서 DB에서 빠져나온다.



14. 9번에서 만든 관리자 ID로 로그인한다. 문제와 테스트케이스를 만들고 저장한다. 추가적인 테스트케이스를 입력하라는 버튼이 나오는데, 이 페이지는 다음에 다시 들어갈 수 있다. 관리자 페이지와 문제 추가에 대한 설명은 생략한다.



참고) 테스트용 문제를 추가할 때, 입력 테스트케이스에 아무 숫자나 입력하고, 출력 테스트케이스에는 Hello 등의 간단한 문자열을 지정한다. 그러면 테스트용 코드를 작성할 때 간단하다. 테스트 케이스를 입력하지 않으면 채점이 되지 않을 수도 있다.



15. 관리자 페이지에서 빠져나와, 문제를 풀고 평가가 제대로 되는지 확인한다. Pending이 지속되지 않고, Accepted 혹은 Wrong Answer, Compile Error 등의 결과가 나오면 성공!


16. 관리 페이지의 문제 리스트에서 Reserved 되어 있는 문제의 status를 클릭하여 Available 상태로 만든다. 이 상태가 되면 관리자가 아닌 일반 유저도 문제를 볼 수 있게 된다.




※ Troubleshootings

- /home/judge/ 디렉터리 내에서 hustoj 관련 파일을 찾을 수 없을 때

install-ubuntu16+.sh 파일 실행 중에 실패한 것이다. 슈퍼유저 권한을 주지 않고 실행했거나, 기존에 설치되어 있던 mysql 또는 nginx 설정과 충돌했을 가능성이 있다. install 파일을 실행할 때 발생한 출력 메시지를 확인하여 에러를 수정한다.


- 웹 사이트가 뜨지 않을 때

에러 메시지에 따라 nginx나 mysql 설정을 확인한다. (구글링..)


- 코드를 제출했는데 pending 상태에서 무한히 대기할 때

ps -ef | grep judged 명령으로 judged 데몬이 실행중인지 확인한다. 실행중이라면 프로세스를 (kill -9 등으로) 죽이고 재실행해 본다. 실행중이 아니라면, 실행한다.(sudo service judged start)

문제가 해결되지 않으면 /home/judge/src/core/judged 경로로 들어가 해당 프로세스를 포그라운드로 실행하여 에러 메시지를 확인 후 조치한다. 파일 접근 권한 문제이거나 DB 접속 문제라면 관련 에러가 뜰 것이다.

만약 아무 에러도 뜨지 않는다면, 관리 페이지에서 테스트케이스 데이터를 확인해 본다.


- 입력 테스트케이스의 문자열 길이가 잘못 측정될 때

테스트케이스 추가페이지에서는 줄바꿈에 \r\n을 사용한다. 이 부분은 관리페이지의 내부 php 코드를 수정하여 캐리지리턴(\r)을 빼고 라인피드(\n)만 저장되도록 수정해야 한다. (이런 잔문제들이 많다..)




HUSTOJ를 사용하여 https://judge.zetagate.com을 운영하고 있었으나, 여러가지 문제가 많고, 확장성이 없다고 판단하여 새로 제작 중이다. https://github.com/ice-judge 아직 Github 저장소만 만들어 놓은 시작 단계다...





 기수정렬(Least Significant Digit 방식) 관련 글들을 찾아보면 개념을 쉽게 설명하기 위해 십진수 단위를(base 10) 사용한 것들이 많은데 실제로 이것을 구현하면 생각보다 느린 것을 볼 수 있다. 나눗셈 연산과 나머지연산이 수행되기 때문이다.  예를 들어 100의 자리에서 기수정렬할 단계를 보면, (현 상태에서 m은 100이라고 가정한다.)


arr[i]/m%10    


위와 같이 / 연산과 % 연산이 사용된다.



 이러한 문제를 개선하기 위해 버킷의 개수를 2의 거듭제곱으로 하면 속도가 빠른 비트연산을 사용할 수 있다. 버킷의 개수를 8로 하면 위 연산식이 아래와 같이 된다. (현 상태에서 m은 64라고 가정)


(arr[i]>>m)&7


2의 거듭제곱으로 나누었을 때의 나머지는, 굳이 나누어서 계산해볼 필요 없이 해당 위치의 비트만 AND연산으로 추출하면 되는 것이다.




구현


 아래 코드는 0부터 99,999,999까지의 정수 데이터들을 정렬할 수 있는 기수정렬(base 8) 알고리즘을 코드로 구현한 것이다. 

     for(int i=0; i<9; ++i) {


           idx = 0;

for(int j=0; j<8; ++j) { counts[j] = 0; } for(int j=0; j<n; ++j) { digit = (arr[j]>>i*3)&7; bucket[digit][counts[digit]++] = arr[j]; } for(int j=0; j<8; ++j) { for(int k=0; k<counts[j]; ++k) { arr[idx++] = bucket[j][k]; } } }


비트시프트 연산을 보면, i는 데이터를 8진수로 나타냈을 때 특정 자리의 숫자를 얻을 때 사용된다. i를 8이 될때까지만 반복시키는 이유는  으로, 99,999,999를 8진수로 나타낼 수 있는 가장 큰 자릿수가 8이기 때문이다.




수행시간 분석


 base 8 기수정렬에서 전체 데이터의 양을 N개라고 했을 때 버킷의 크기는 bucket[8][N]이다. base 8 대신 base 4나 base 2를 사용하면 메모리 사용량은 각각 2배, 4배 줄어들게 된다. 반면, 기수정렬의 시간복잡도가 O(dN)이므로, 수행시간은 더 늘어난다. (d는 해당 base에서 데이터의 최대 자릿수이다.) 표 1은 내림차순으로 정렬된 100만개 ~ 500만개 데이터를 오름차순으로 정렬했을 때 각 알고리즘의 수행 시간(ms)이다. 표 2는 랜덤함수로 생성한 무작위 데이터를 오름차순으로 정렬했을때의 결과이다.


표 1


표 2




 표 1의 결과를 그래프로 나타내 보면 다음과 같다.




 base 8 대신 16이나 32를 사용하면 메모리 사용량이 늘어나고, 속도가 약간 줄어들겠지만, 값 복사시간이 기본적으로 존재하기 때문에 속도가 줄어드는 데에는 한계가 있다. 수행시간이 카운팅 정렬을 사용했을때 이하로 떨어지지는 않을 것이다.


 Dovelet이나 BOJ 같은 사이트들을 풀어보면서 직접 문제들을 만들어 보고 싶었고, 알고리즘을 공부하는 사람들과 소규모 대회를 열어보기 위해 온라인저지(Online Judge)시스템을 구축하게 되었다.




DMOJ 설치


 DMOJ는 Django 기반으로 되어 있는 온라인저지 시스템으로, github 커밋이 꾸준히 올라오고 있고 현대적인 사이트 디자인이 특징이다. 설치 가이드에 따라 설치를 진행하였는데 마지막 단계에서 nginx 권한 관련 오류를 해결하지 못하여 진행하지 못하였다. 도커이미지도 제공되고 있어서 컨테이너를 실행해 보았더니 이번에는 Django 내부 코드에서 오류가 발생하여 포기.




HUSTOJ 설치



* 2018년 2월에 갱신되었습니다. 아래 글 보다는 이 글을 참고하세요.


 일단 구동이 되는 것을 찾다가 HUSTOJ를 설치해보기로 하였다. HUSTOJ는 교내 프로그래밍 대회에 사용되고 있고, CodeUp 및 여러 온라인저지에서 사용하고 있는 오픈소스 프로젝트이다. 채점기는 C++로 짜여져 있으며 웹페이지는 PHP로 구동된다. 설치할 때 공식 페이지 블로그 포스팅을 참고하여 진행하였다. 설치 과정은 아래와 같다.


1) install/install.sh에서 WEBBASE, APACHEUSER, DBUSER, DBPASS, core 디렉터리 경로, web 디렉터리 경로를 환경에 맞게 설정한다. subversion 관련 코드들은 삭제한다. 


2) install/judge.conf를 확인한다. OJ_HOST_NAME, OJ_USER_NAME, OJ_PASSWORD와 MySQL관련 설정들을 환경에 맞게 수정하면 된다.


3) web/include/db_info.inc.php 파일에서 원하는 설정을 한다. 이 부분은 설치 후에도 변경할 수 있다.


4) install.sh를 실행하여 설치를 진행한다. 이 프로젝트는 Ubuntu 14에서 테스트되었고, Ubuntu 16을 비추천한다고 하는데, 16에서도 잘 구동된다. php5 대신 php7을, apache대신 nginx를 이용하는 것은 install 디렉터리에 함께 있는 ubuntu16용 설치 sh파일을 참고한다. sh파일을 실행하면,

/etc/nginx/site-enabled에서 root은 /home/judge/src/web이 되고, php관련 location설정은

       location ~ \.php$ {

                include snippets/fastcgi-php.conf;

        #

        #       # With php7.0-cgi alone:

        #       fastcgi_pass 127.0.0.1:9000;

                fastcgi_pass unix:/run/php/php7.0-fpm.sock;

        }

와 같이 작성된다.




사이트 수정


 레이아웃을 비롯하여 회원가입, 관리자페이지, 채점기 등 많은 부분을 수정하였다. judge 기본 디렉터리에서 data에는 문제의 테스트케이스가 있고, src/core에는 채점 프로그램 관련 소스와 makefile이 존재한다.

 웹페이지는 src/web 디렉터리에 있는데, src/web에는 SQL에서 데이터를 받아오는 부분이 있고,  src/web/template/bs3에는 주로 뷰 관련 코드(html)가 들어 있다. 그러나 잘 분리되어 있지 않아 수정이 번거롭다.

 PHP와 SQL, html이 섞여 있어 구조적으로 알아보기 불편하고, SQL튜닝이 잘 되어있지 않아 데이터가 많아지면 상당한 오버헤드가 예상된다. 일단 이 프로젝트를 사용하여 회원탈퇴, 소스공유, 랭킹시스템 등 필수적인 기능을 구현할 것이다. 그리고 저지사이트 구축을 공부해볼겸 하여 기존의 코드를 버리고 프레임워크를 새로 만들어 보기로 계획하였다.


현재 진행중인 사이트는 여기다.






2차원 평면상에서 선분 AB와 CD의 정보가 주어질 때, 두 선분의 교차 여부를 빠르게 판단할 수 있는 방법이 있다. 벡터의 외적(벡터곱)을 이용하는 방법이다.


그림 1과 같이 두 선분이 교차하는 경우를 생각해 보자. 벡터 BA를 기준으로 점 C와 점 D가 서로 다른 회전방향에 있으므로, 아래 두 경우는 벡터곱 연산의 결과로 나온 z값의 부호가 다르다.

- 벡터 BA와 벡터 BC를 외적한 결과

- 벡터 BA와 벡터 BD를 외적한 결과


벡터 DC에 대해서도 역시 부호가 다르게 된다.

- 벡터 DC와 벡터 DA를 외적한 결과

- 벡터 DC와 벡터 DB를 외적한 결과 


     

그림 1. 교차하는 두 선분                                  


즉, 벡터곱 연산을 이용하여 한 선분의 벡터를 기준으로, 다른 선분의 두 점들이 같은 회전뱡향에 있는지, 다른 회전방향에 있는지 검사하면 교차 여부를 판단할 수 있는 것이다.
두 선분에 대해 이러한 판단을 수행하여 모두 만족하면 교차하는 것으로 판정한다.


그림 2에서는 벡터 CD에 대해 다른 선분의 두 점(A, B)는 서로 다른 회전방향이지만, 벡터 AB에 대해서는 두 점(C ,D)가 같은 회전방향에 있기 때문에 교차하지 않는 것으로 판정한다.


 

그림 2. 교차하지 않는 두 선분



Javascript로 표현하면 아래와 같이 쓸 수 있다.

    let p1 = line1.p1;

    let p2 = line1.p2;

    let p3 = line2.p1;

    let p4 = line2.p2;


    let sign1 = (p2.x-p1.x)*(p3.y-p1.y) - (p3.x-p1.x)*(p2.y-p1.y);

    let sign2 = (p2.x-p1.x)*(p4.y-p1.y) - (p4.x-p1.x)*(p2.y-p1.y);


    let sign3 = (p4.x-p3.x)*(p1.y-p3.y) - (p1.x-p3.x)*(p4.y-p3.y);

    let sign4 = (p4.x-p3.x)*(p2.y-p3.y) - (p2.x-p3.x)*(p4.y-p3.y);


    return sign1*sign2<0 && sign3*sign4<0;




그러나 위 코드에는 문제점이 있는데, 그림 3과 같이 두 선분의 기울기가 같을 때는 교차하지 않는 것으로 판단한다는 것이다. (이런 결과를 의도했다면 문제가 되지 않겠지만..)


그림 3. 두 선분의 기울기가 같고, 교차점이 무수히 많은 경우



그림 3과 같은 경우에도 두 선분이 교차하는 것으로 판정하기 위해, 비교문을 아래와 같이 고치면 또다른 문제가 발생한다. 그림 4와 같이 기울기가 같고 서로 떨어져 있을 때 잘못 판정한다는 것이다.

return sign1*sign2<=0 && sign3*sign4<=0;



그림 4. 교차 여부를 잘못 판정한 경우




두 선분이 완전히 떨어져 있을 경우(더 정확히 말하면, 각각의 선분이 이루는 직사각형 영역이 서로 겹치지 않는 경우)에 false를 리턴하면 이 문제를 해결할 수 있다. 외적 연산에 앞서 사전 영역 검사를 하는 것이다.

아래 코드에서 모든 부호가 0일 때 true를 반환하는 부분을 추가한 이유는, 그림 3과 같이 교차점이 무수히 많은 경우도 교차하는 것으로 판단하기 위해서이다.


    let p1 = line1.p1;

    let p2 = line1.p2;

    let p3 = line2.p1;

    let p4 = line2.p2;


    if(Math.max(p1.x, p2.x) < Math.min(p3.x, p4.x)) return false;

    if(Math.min(p1.x, p2.x) > Math.max(p3.x, p4.x)) return false;

    if(Math.max(p1.y, p2.y) < Math.min(p3.y, p4.y)) return false;

    if(Math.min(p1.y, p2.y) > Math.max(p3.y, p4.y)) return false;


    let sign1 = (p2.x-p1.x)*(p3.y-p1.y) - (p3.x-p1.x)*(p2.y-p1.y);

    let sign2 = (p2.x-p1.x)*(p4.y-p1.y) - (p4.x-p1.x)*(p2.y-p1.y);


    let sign3 = (p4.x-p3.x)*(p1.y-p3.y) - (p1.x-p3.x)*(p4.y-p3.y);

    let sign4 = (p4.x-p3.x)*(p2.y-p3.y) - (p2.x-p3.x)*(p4.y-p3.y);


    if(sign1==0 && sign2==0 && sign3==0 && sign4==0) return true;


    return sign1*sign2<=0 && sign3*sign4<=0;



그림 5. 교차 여부를 정상적으로 판정한 경우




경우에 따라서 그림 6과 같이 선분의 끝점이 교차할 때는 교차하지 않는 것으로 판단하는 것이 필요할 때가 있다.

 

그림 6, 7. 두 선분이 교차하는 것으로 판단하는 경우



아래 코드에서와 같이 사전 영역 검사에 동등 조건을 넣어  좀더 엄격하게 바꾸고(그림 6과 관련), 마지막의 리턴 코드에서 동등 조건을 제거한다.(그림 7과 관련)


    let p1 = line1.p1;

    let p2 = line1.p2;

    let p3 = line2.p1;

    let p4 = line2.p2;


    if(Math.max(p1.x, p2.x) <= Math.min(p3.x, p4.x)) return false;

    if(Math.min(p1.x, p2.x) >= Math.max(p3.x, p4.x)) return false;

    if(Math.max(p1.y, p2.y) <= Math.min(p3.y, p4.y)) return false;

    if(Math.min(p1.y, p2.y) >= Math.max(p3.y, p4.y)) return false;


    let sign1 = (p2.x-p1.x)*(p3.y-p1.y) - (p3.x-p1.x)*(p2.y-p1.y);

    let sign2 = (p2.x-p1.x)*(p4.y-p1.y) - (p4.x-p1.x)*(p2.y-p1.y);


    let sign3 = (p4.x-p3.x)*(p1.y-p3.y) - (p1.x-p3.x)*(p4.y-p3.y);

    let sign4 = (p4.x-p3.x)*(p2.y-p3.y) - (p2.x-p3.x)*(p4.y-p3.y);


    if(sign1==0 && sign2==0 && sign3==0 && sign4==0) return true;


    return sign1*sign2<0 && sign3*sign4<0; 





결론


만들고자 하는 프로그램에 따라 선분 끝점을 어떻게 판단해야할지가 달라진다. 위 코드를 활용하여 비교문들을 조정하면 선분의 교차 판단을 목적에 맞게 할 수 있을 것이다.



 

소스코드


동작하는 전체 코드는(Javascript)는 아래 github에서 다운로드할 수 있습니다.

https://github.com/tibyte/algorithms/tree/master/line-intersection







브랜치명이 다른 원격 브랜치에 push해야 하는 경우에는

아래와 같은 형식으로 쓰면 된다.

git push 원격저장소명 로컬브랜치:원격브랜치


예시)

git push origin patch:gh-pages






원격 저장소로부터 pull을 받는 경우 

아래와 같이 순서를 바꾸어서 쓴다.

git pull 원격저장소명 원격브랜치:로컬브랜치


만약 대상 브랜치가 HEAD가 가리키고 있는 브랜치이면 로컬 브랜치를 생략하여

git pull 원격저장소명 원격브랜치

와 같이 쓸 수 있다.




 리눅스 환경에서 C언어를 사용하여 빠른 프로세스간 통신(IPC)를 구현해야 할 때에 공유메모리를 사용할 수 있다. 그러나 한 프로세스가 데이터를 읽는 도중에 다른 프로세스가 해당 공간에 대해 쓰기 작업을 한다면 데이터 부정합이 발생할 수 있다. POSIX 세마포어 중 이름 있는 세마포어(named semaphore)를 사용하여 공유자원에 대한 동시 접근 문제를 해결할 수 있다.


 공유메모리를 사용하기 위해 아래와 같은 함수들을 사용해야 한다.

shmget() 

임의의 키값과 메모리크기를 매개면수로 주어 공유메모리 생성을 커널에 요청하고, 정상적으로 생성되었을시 id를 받아온다. 


shmat()

공유메모리를 현재 프로세스에 붙인다(attach) 반환값으로는 공유메모리의 포인터가 나오고 이 포인터를 통해 메모리에 접근할 수 있다.


shmdt()

공유메모리를 현재 프로세스에서 분리한다(detach)


shmctl()

공유메모리에 대한 제어를 한다. 여기서는 삭제를 위해 사용.

생성된 공유메모리에 대한 정보는
ipcs -m
명령으로 확인할 수 있다.




POSIX named 세마포어는 아래 함수들을 통해 사용할 수 있다.


sem_open()

named 세마포어를 생성하고 세마포어 구조체 포인터(sem_t *)를 받아온다.


sem_wait()

공유자원에 접근하기 전에 세마포어를 잠근다. 만약 해당 자원에 접근할 수 있는 프로세스가 최대 1개이고, 잠겨있는 상태이면 block되어 풀릴 때까지 대기한다.


sem_trywait()

sem_wait()의 논블로킹 함수로, 자원이 잠겨 있어도 일단 -1을 반환하며 함수가 종료되고, errno를 통해 추가적인 처리를 할 수 있다. 


sem_post()

공유자원에 대한 접근이 끝났을 때 세마포어 잠금을 해제한다.


sem_close()

해당 프로세스에서 세마포어를 닫는다.


sem_unlink()

세마포어를 파괴한다.



예제코드는 아래와 같이 컴파일하였다.

clang provicer.c -o provider -std=gnu99 -lpthread


provider.c

#include <stdio.h>

#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

int main(void)
{
    int shmid;
    size_t shsize = 1024;
    const int key = 16000;
    char *shm;

    sem_t *mysem;
    sem_unlink("mysem");
    if((mysem = sem_open("mysem", O_CREAT, 0777, 1)) == NULL) {
        perror("Sem Open Error");
        exit(1);
    }

    if((shmid = shmget((size_t)key, shsize, IPC_CREAT|0666))<0) {
        perror("shmget");
        exit(1);
    }

    if((shm = (char*)shmat(shmid, NULL, 0)) == (char*)-1) {
        perror("shmat");
        exit(1);
    }

    for(int i=0; i<100; i++) {
        shm[i] = 0;
    }


    for(;;) {
        sem_wait(mysem);
        for(int i=0; i<100; i++) {
            shm[i] = (shm[i]+1)%10;
        }
        usleep(1000*1000);
        sem_post(mysem);
        usleep(1000*1000);
    }

    getchar();

    sem_close(mysem);

    sem_unlink("mysem");

    shmdt(shm);
    shmctl(shmid, IPC_RMID, 0);


    return 0;
}


consumer.c

#include <stdio.h>

#include <unistd.h>
#include <semaphore.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

int main(void)
{
    int shmid;
    size_t shsize = 1024;
    const int key = 16000;
    char *shm;

    sem_t *mysem;
    if((mysem = sem_open("mysem", 0, 0777, 0)) == SEM_FAILED) {
        perror("Sem Open Error");
        exit(1);
    }

    if((shmid = shmget((key_t)key, shsize, IPC_CREAT|0666))<0) {
        perror("shmget");
        exit(1);
    }

    if((shm = (char*)shmat(shmid, NULL, 0)) == (char*)-1) {
        perror("shmat");
        exit(1);
    }

    for(;;) {
        if(sem_trywait(mysem) == 0) {
            for(int i=0; i<100; i++) {
                printf("%d", (shm[i]));
            }

            putchar('\n');
            sem_post(mysem);
        }
        else {
            switch(errno) {
                case EAGAIN:
                    printf("EAGAIN\n");
                    break;
                case EDEADLK:
                    printf("EDEADLK\n");
                    break;
                case EINTR:
                    printf("EINTR\n");
                    break;
            }
        }

        usleep(100*1000);
    }
    

    sem_close(mysem);
    shmdt(shm);

    return 0;
}



출처 

Koreatech Online Judge(링크)


문제이해 

양의 정수를 1부터 일렬로 나열했을 때 (ex 123456789101112...) P번째에 오는 숫자를 구하는 문제이다.


문제접근 

자리수가 1개인 수(1~9), 2개인 수(10~99), 3개인 수(100~999), ... 를 구분하여 계산한다


구현 

아래와 같은 규칙이 있다.

- 자리수가 1개인 수를 모두 썼을 때는 길이가 9인 문자열이 생성된다.

- 자리수가 2개인 수를 모두 썼을 때는 길이가 90*2 = 180 인 문자열이 생성된다.

- 자리수가 3개인 수를 모두 썼을 때는 길이가 900*3 = 2700인 문자열이 생성된다.

만약 P 값으로 2889가 입력된다면, 그 수는 9+180+2700 이므로 세 자리 숫자 중 가장 마지막에 오는 숫자(999)일 것이다.


같은 방법으로, 임의의 P가 있을 때 이 수가 몇 자리 수로 이루어진 정수까지 썼을 때 나온 길이인지 계산할 수 있으며, 해당 자리 수를 가진 수 중 가장 작은 수까지 썼을 때 문자열의 길이도 구할 수 있다.


1부터 n까지 수를 일렬로 나열했을 때 문자열의 길이를 f(n)이라 한다면,

예를 들어, f(1000)과 f(10000)을 미리 계산할 수 있으므로, P가 이 사이의 값이라면 P는 네 자리 수까지 썼을 때의 값이다. 이 P값에서 f(1000)을 뺀 후 4로 나누고, 1000을 더하면, n값을 알 수 있는 것이다.  


실행결과 


코드
https://github.com/tibyte/algorithm-problems/blob/master/koreatech/1068/1068.cpp


xor연산자를 사용하여 Javascript로 canvas에 간단한 패턴 이미지를 그려보았다.


   for(var i=0; i<height; i++) {

     for(var j=0; j<width; j++) {

       pixels[i*width + j] = (i^j)%256;

     }

   } 






xor연산자 대신 atan2()함수를 사용한 결과


    for(var i=0; i<height; i++) {

        for(var j=0; j<width; j++) {

            var dist = getDist(j-width/2, i-height/2);  //거리

            var dir = getDir(j-width/2, i-height/2);  //각도

            pixels[i*width + j] = (256-dist)/(dir/96);

        }

    } 




전체 소스코드 : https://github.com/tibyte/visual/tree/master/xor




출처

Koreatech Onlinejudge(링크)



문제이해

-50000이상 50000이하 정수로 이루어진 배열이 입력된다. 두 개의 수를 제외한 나머지 수는 모두 2개씩 존재한다. 단 하나씩만 존재하는 두 수를 크기가 작은 것부터 출력하는 문제이다.



문제접근

유일한 수를 찾는 문제에서는(링크) 모든 배열원소들을 XOR연산으로 누적하여 쉽게 구할 수 있었다. 그러나 이 문제에서는 2개를 찾아야 하므로 단순히 XOR로는 구할 수 없다. 


1. 배열 이용

입력되는 수의 범위가 -50000부터 50000까지 100001크기의 범위이므로 크기 100001짜리 배열을 만들고, 모든 인자를 0으로 초기화한 후, 입력배열을 순차탐색하면서 해당 입력값을 인덱스로 하는 배열위치에 1을 더한다. 이 과정이 끝났을 때 1이 기록되어 있는 위치를 출력하면 된다. 이 방법의 한계는 입력되는 수의 범위가 더 클 경우 적용할 수 없다는 것이다.


2. std::map 이용

입력되는 수를 std::find()로 찾아서 값이 있으면 std::erase()로 지우고 없으면 추가한다.

최종적으로 남은 요소를 출력하면 된다

std::map은 rbtree로 되어있기 때문에 각 연산이 O(logn)이며 입력의 수만큼 반복하게 되므로 복잡도는 O(nlogn)이 된다.


3. 비트연산(XOR) 이용

(참고 : http://www.geeksforgeeks.org/find-two-non-repeating-elements-in-an-array-of-repeating-elements/)

XOR연산으로도 이 문제를 풀 수 있다. 우선 모든 수를 XOR연산으로 누적하면 하나의 수가 나오는데, 이것은 답으로 출력해야 할 두 수의 XOR연산결과이다. 이 결과에서 rightmost set bit(낮은 비트부터 봤을 때 1이 처음으로 나타나는 위치)을 찾는다.


예를 들어 XOR결과가 이진수로 1101000라고 하자. rightmost set bit을 제외한 나머지 비트들을 0으로 하면 0001000이 된다. 이것은 모든 입력값들 중에서 해당 위치에 1이 쓰여진 원소가 홀수 개라는 뜻이 된다. 즉, 구하고자 하는 답 중에서 하나는 해당 위치에 1이 쓰여진 원소들 중에서 나오고, 다른 하나는 해당 위치에 0이 쓰여진 원소들 중에서 나온다는 것이다.


이제 다시 모든 원소들을 위에서 구한 XOR값(위 예시에서는 0001000)과 비트 AND 연산을 하면서, 결과가 1인 경우와, 결과가 0인 경우로 나누어 각각 XOR연산으로 누적한다. 

 for(int i=0; i<N; ++i) {

if(arr[i]&det) {

xora ^= arr[i];

}

else {

xorb ^= arr[i];

}

}

문제의 조건에 따라, 구한 두 값을 크기순서로 출력하면 된다.



구현

이 문제에서 필요한 rightmost set bit의 형태는 위치값이 아니라, 해당 비트만 1로 세팅된 수이기 때문에 아래와 같은 간단한 연산으로 구할 수 있다.  (1101000을 예시로 하여 직접 계산해 보면 이해할 수 있다.)

num&(~num+1)


이 연산을 잘 보면, 2의보수를 구하고 있는 형태이기 때문에

num&-num 으로도 쓸 수 있다.



코드
https://github.com/tibyte/algorithm-problems/blob/master/koreatech/1074/1074.c

출처

Koreatech OnlineJudge(링크)



문제이해

5000자 이하의 문자가 입력으로 주어지고, 임의의 위치에 문자를 추가하여 펠린드롬으로 만들 때 추가하는 문자의 개수를 최소화하면, 몇 개인지를 구하는 문제이다.



문제접근

동적 프로그래밍 문제이다.

인덱스 a부터 인덱스 b까지의 구역에 대해 펠린드롬을 만들기 위한 최소비용을 D[a][b]라고 하자. D[a][b]를 한 단계 작은 계산결과로 구하기 위해 두 가지 조건을 따져봐야 한다.


1. 구역의 맨 앞과 맨 뒤가 같은 문자인 경우

D[a][b] (구역의 전체)는 D[a+1][b-1] (회색 부분)와 같다.



2. 구역의 맨 앞과 맨 뒤가 다른 문자인 경우

D[a][b] = 1+min(D[a][b-1], D[a+1][b])

a부터 b-1까지의 부분구역과, a+1부터 b까지의 부분구역 중에서 비용이 작은 것을 선택하여 거기에 1을 더한 값을 D[a][b]로 한다.


Top-down 방식으로 점화식을 해결하면 시간복잡도가 지수함수가 되므로, 아래에서부터 누적하여 구해야 한다.


구간의 길이를 1부터 전체까지 늘려 가며 데이터를 얻는 것이다.

전에 포스팅했던 이 문제와 같은 아이디어이다.





구현

다음과 같이 전처리하면 복잡하지 않게 반복문을 짤 수 있다.

- 구간의 길이가 1인 경우 D값은 0으로 한다.

- 구간의 길이가 2인 경우 D값은, 두 문자가 같을 때 0, 다를 때 1로 한다.



코드
https://github.com/tibyte/algorithm-problems/blob/master/koreatech/1060/1060.cpp



+ Recent posts