숏코딩 - 코드로 골프하기

숏코딩(a.k.a. 코드골프)이란?

백준저지의 또다른 재미 중 하나가 숏코딩입니다. 숏코딩이란, 어떤 문제를 되도록 짧은 코드로 푸는 게임입니다. 여기서 '짧은 코드'의 기준은 코드의 글자수(바이트)로 판단하게 됩니다. 따라서 문제를 푸는 능력은 물론 프로그래밍 언어의 특징을 알고 이를 활용하는 능력까지 기를 수 있는 게임입니다. 숏코딩에 유리한 프로그래밍 언어가 따로 있는데, 대표적으로 펄이 가장 널리 쓰이고, 루비, 파이썬등의 스크립트 언어나 sed/awk도 자주 쓰입니다. 또한 숏코딩을 위하여 고안한 언어(Golfscript)도 있으며, (다만, 현재 백준저지에선 지원 안합니다...) 심지어 아희로도 가능합니다(!)

C 언어로 골프하기

※ 참고: 코드예제 밑에 나오는 크기(바이트)는 들여쓰기나 개행문자등에 따라 다른 값이 다를 수 있습니다. 또한 주석은 계산하지 않았습니다.

백준저지 2558번 "A + B - 2" 문제를 예로 들어보겠습니다. 이 문제는 두 수를 입력받아 더한 값을 출력하는 쉬운 문제입니다.

먼저, 가장 널리 쓰이는 언어 C로 이 문제를 (평범하게) 풀어보겠습니다.

#include <stdio.h>

int main () {
  int a, b;
  scanf("%d%d", &a, &b);
  printf("%d", a + b);
  return 0;
}

(104바이트)

음, 말 그대로 평범합니다. 여기서부터 조금씩 압축해봅시다. 표준에 맞게 main함수를 int main으로 정의했는데, 사실 int부분이 없어도 동작합니다.(이는 K&R이라고도 하는, 옛날 C언어 스타일 입니다.) 또한, return 0또한 생략할 수 있습니다. 이 두가지를 빼보죠.

#include <stdio.h>

main () {
  int a, b;
  scanf("%d%d", &a, &b);
  printf("%d", a + b);
}

(89바이트)

네, 좀 줄었네요. intreturn 0을 줄였으니 약 12바이트를 줄였습니다. 한 줄 더 지울수 있는데, (사실 저도 이 글을 쓰면서 처음 안 건데) 바로 #include <stdio.h>를 생략하는 겁니다. 컴파일 과정 중 링커가 C 라이브러리를 링크하기 때문이라네요. (참고: http://stackoverflow.com/a/336844) 따라서, 이 줄도 지워보겠습니다.

main () {
  int a, b;
  scanf("%d%d", &a, &b);
  printf("%d", a + b);
}

(69바이트)

이제 함수만 남았네요. 자, 그 다음엔 어떻게 줄일까요? main함수가 사실 두 인수를 받는 함수라는거 알고 계시죠? 아마 main (int argc, char **argv)같은 형태의 함수를 본 적 있을 겁니다. 또한 저 예제코드에서 선언한 변수도 2가지네요. 그렇다면?

main (a, b) {
  scanf("%d%d", &a, &b);
  printf("%d", a + b);
}

(62바이트)

더 줄었네요. 참고로, 인자에 타입을 생략하는 것 역시 K&R스타일입니다. 그리고 포인터 특성상 정수값을 대입할 수 있습니다. 이제 어느정도 줄인 것 같으니 가독성을 위해 쓴 들여쓰기와 띄어쓰기를 모두 빼봅시다.

main(a,b){scanf("%d%d",&a,&b);printf("%d",a+b);}

(48바이트)

48바이트입니다. 맨 처음 코드가 104바이트이니까, 절반 넘게 줄였네요! 주로 K&R이라는, 옛 방식의 C언어 스타일을 이용하여 줄였습니다. 이처럼 프로그래밍 언어의 어두운(...) 구석을 알면 코드를 많이 줄일 수 있습니다.

Perl로 골프하기

이번엔 숏코딩에 가장 널리 쓰이는 언어 중 하나인 펄(Perl)을 써봅시다. 예제문제는 백준저지 9012번 - "괄호"문제입니다. 이 문제는 괄호 문자열이 짝이 맞는지를 판단하여 YES나 NO로 출력하는 문제입니다. 원랜 이 문제는 스택을 이용하여 푸는게 정석이겠지만, 다른 방법을 생각해봅시다. 괄호 짝이 맞는 문자열이라면 "()"(문제에서 '기본 VPS'라고 함)가 있을 겁니다. 그리고, 그 문자열에서 ()를 하나 지워도 다시 ()가 생깁니다. 계속 ()를 전부 지우고 난 뒤, 글자가 안 남으면 짝이 맞는 문자열, 괄호 기호가 하나라도 남는다면, 그게 바로 짝이 안 맞는 괄호일 것입니다. 이를 펄로 짜보겠습니다.

<>;
while (<>) {
    chomp;
    while (s/\(\)//g) {}
    print $_ ? "NO\n" : "YES\n";
}

(79바이트)

아마 펄에 익숙하지 않다면, 코드를 이해하기 어려울 수도 있습니다. 일단 <>는 Standard Input에서 한 줄을 입력받습니다. (이 때, 개행문자 \n도 포함입니다.) while (<>) 형태로 쓰면 한 줄씩 입력 받으면서 안에 있는 구문을 실행하게 됩니다. 그런데 변수가 안 보이네요?

Topic Variable

펄에는 마법의(!) 변수가 참 많이 있습니다. 그 중 대표적인게 $_입니다. 이는 펄의 함수나 정규식을 사용할 때, 변수를 지정하지 않으면 기본값으로 적용대상이 되는 변수입니다. (Clojure나 Scala에도 비슷한 개념이 등장합니다.) 예를 들어, 예제 코드에 chomp함수는 문자열 끝의 개행문자를 지우는 용도로 사용합니다. 하지만, 변수를 지정하지 않았으니 $_를 처리할 것입니다. 따라서 위에 코드에 chomp;chomp($_);와 같은 동작을 하게 됩니다. 정규식도 마찬가지인데, 변수를 지정하지 않으면 $_에서 정규식 패턴을 찾거나 치환을 하게 됩니다. 저 위에 s/\(\)//g$_ =~ s/\(\)//g와 같은 동작을 하게 됩니다. (참고로, 펄에서 =~연산자는 정규식을 적용할 때 쓰이는 연산자입니다.)

정규표현식

s/\(\)//g는 정규표현식으로 문자열을 치환하는 식입니다. 정규표현식에 대한 설명은 이 글의 주제를 벗어나는 것 같으니 생략하고, 여기선 ()문자를 빈 문자열로 바꾸는(즉, 없애는) 식이라고 이해하시면 됩니다. 참고로, 펄에선 정규표현식 치환을 쓰면 문자열 치환 횟수가 반환합니다. 예를 들면 저 식으로 두 개의 ()를 찾았으면, 이를 지우고 2를 반환합니다. 아무것도 찾지 못했을 때는 0을 반환하게 되는데, 이를 while문에 넣어서 while (s/\(\)//g) {}코드는 "'()'가 없어질 때까지 이를 없애라"라는 명령이 되는 겁니다. 문자열만 바뀌면 되므로 {}안쪽엔 그냥 비워놨습니다.

자 여기서 코드를 줄일 수 있는 전략이 하나 더 있습니다. 바로 while을 뒤에 쓰는 겁니다. 펄 문서(perldoc)에선 이렇게 나와있습니다.

Any simple statement may optionally be followed by a SINGLE modifier, just before the terminating semicolon (or block ending).

출처: perlsyn - Statement Modifiers

앞서 chomp함수는 줄끝문자를 지우는 함수라고 했습니다. 어차피 줄끝문자는 필요없고, 몇 번을 지우던 똑같은 결과가 나오므로, 이를 합쳐서 다음과 같이 사용할 수 있습니다.

<>;
while (<>) {
    chomp while s/\(\)//g;
    print $_ ? "NO\n" : "YES\n";
}

(73바이트)

chomp while s/\(\)//g; 이 한줄로 입력받은 문자의 줄끝문자를 지우고 괄호를 없애는 동작을 한 줄에 끝내버릴 수 있습니다. 참고로, ifwhile, for등을 뒤에 적을 때는 조건부 부분의 괄호를 생략할 수 있습니다. 이제 여기까지 적은 코드에 들여쓰기/띄어쓰기를 지워서 압축해봅시다.

<>;while(<>){chomp while s/\(\)//g;print$_?"NO\n":"YES\n"}

(58바이트)

결과 58바이트 나왔습니다. (참고로 이 코드는 실제로 제가 제출한 코드입니다.) 상당히 많이 줄어든 모습입니다. :)

결론

숏코딩(코드골프)를 통해 알고리즘 뿐만 아니라, 프로그래밍 언어 고유의 특징을 알고 활용하는 능력을 기를 수 있습니다. 물론, 저렇게 줄어든 코드는 가독성은 물론, 비표준에 유지보수하기 힘드므로 실제 프로그램을 작성할 때 쓰이진 않지만, 코드를 한자한자 줄여가면서 코딩하는 것도 재밌는 게임입니다. 여러분 모두 짧고(?) 즐거운 코딩하세요~!


댓글 (3개) 댓글 쓰기


h0ngjun7 8년 전

재밌네요 ㅋㅋㅋㅋ


po10003 8년 전

왜 A+B는 되면서 A+B2는 안될까요? main(n){gets(&n);printf("%d",n%85-43);}


zinc 8년 전

C의 gets함수가 한 줄만 읽어서 그럴것 같은데요. 1000번 문제 "A+B"의 경우, 한 줄에 두 숫자가 주어진 반면에, 2558번 문제 "A + B (2)"의 경우엔 각 숫자가 두 줄에 나뉘어져 있어요.