MagicCircle

코드 골프라고도 불리는 숏코딩은, 코드를 최적화나 가독성을 고려하지 않고, 최대한 적은 바이트로 코드를 작성하는 일종의 코딩 스타일 입니다. 해외에서는 이 숏코딩을 일종의 대회나 경쟁 요소로도 활용됩니다. 국내에서도 알고리즘 문제 해결 사이트인 백준 온라인 저지에서 문제 해결을 위해 사용한 바이트 수를 가지고 순위를 매기기도 합니다. 오늘은 C에서 숏코딩을 하는 방법과, 사용되는 테크닉에 대해서 알아보겠습니다.

1. 숙련된 조교의 시범

예시 코드를 보며 배워봅시다. 예시로 백준 온라인 저지 2480번: 주사위 세개에서 rhdqor213의 코드를 해설해보겠습니다.

1
a[7];main(t){while(~scanf("%d",a))t=fmax(t,a[*a]++*9+*a);printf((t<9)+"1%d%0*d0",t%9,t/9,0);}

역시 숏코딩 답게 평균적으로 500바이트가 나오는 문제에서 93바이트로 해결했습니다! 여기서 들어나는 C언어의 특징중 하나로, C언어는 들여쓰기를 할 필요 없습니다. 세미콜론으로 행들을 구분해주면 정상적으로 작동합니다.

일단은 해설을 위해 들여쓰기를 해주도록 합시다.

1
2
3
4
5
6
a[7];
main(t){
  while(~scanf("%d", a))
    t=fmax(t, a[*a]++ * 9 + *a);
  printf((t<9) + "1%d%0*d0", t%9, t/9, 0);
}

1.1. #include <stdio.h> 생략

코드를 보면, 표준 입력, 출력 라이브러리를 불러오지 않았습니다. 하지만, 이는 백준 온라인 저지에서 C99를 컴파일 할때, GCC를 사용하기 때문에, 이후 scanf, printf를 사용해도 오류가 나지 않습니다.

1.2. 전역 변수와 함수의 자료형 생략

전역 변수나 함수 선언시, 이를 정수형으로 선언 하고 싶다면, int를 생략해도 됩니다! 변수 타입을 선언하지 않을때, 기본으로 int형을 부여하기 때문에, 과감히 포기 할 수 있습니다.

1.3. 전역 변수의 배열은 자동으로 초기화

전역 변수는 데이터 영역에 저장되어 선언시 초기화를 하지 않아도, 자동으로 0이 저장됩니다.

1.4. 변수 이름은 최대한 짧게

변수 이름에 큰 투자를 할 필요 없습니다! 숏코딩을 시작한 순간 가독성은 버렸다고 생각하고, C의 변수 이름 짓기 규칙을 따라 알파벳 대, 소문자나 언더바로 한글자 변수를 지어야 합니다.

1.5. main함수에 파라미터로 변수 설정

main 함수에 파라미터를 적용 할 수 있습니다. 원래 main함수의 파라미터는 다음과 같습니다.

1
int main(int argc, char **argv, char **env);

int argcmain함수를 호출 할때 사용된 파라미터의 개수, char **argv는 호출시 사용된 파라미터, char **env는 환경 변수입니다. 하지만, 이를 모두 무시하고 변수를 사용 할 수 있습니다. 하지만, 첫번째 변수 외에는 쓰레기 값이 들어오므로, 초기화가 필요합니다.

1.6. scanf~

scanf는 함수 실행 후 반환값으로 성공적으로 읽은 인자를 반환합니다. scanf("%d", a)에서 a1이 들어왔으면, 반환값으로 1이 반환되는 방식입니다. 하지만, 읽기 오류가 발생한다면 EOF1반환합니다. 그리고 이 EOF는 정수로 -1이므로, 비트 연산자중 ~와 더해진다면, 0이 됩니다.

즉, while(~scanf("%d", a)){...}는 값이 들어올때까지 입력을 받고, 값 입력이 멈춘다면, 0을 반환하여 while문을 종료합니다.

1.7. 배열의 첫 요소를 입력 받을땐

분명 a는 배열입니다. 하지만, scanf("%d", a)를 하면 어떻게 될까요?

문자열을 입력받을 때와 비슷하게, a는 배열을 가리키는 포인터 입니다. 그것도 배열의 첫 요소인 a[0]을 가리킵니다. 그래서 scanf("%d", &a[0])와 같은 효과를 냅니다.

1.8. fmax?

fmaxmath.h2에 포함된 함수중 하나로, double형 변수 두개를 비교하여 더 높은 변수를 반환합니다. int형도 마찬가지로 변수 형 확장을 거친 후, 정상적으로 사용됩니다.

다음 코드와 같은 의미를 가집니다.

1
2
if(t < a[*a]++ * 9 + *a)
   t = a[*a]++ * 9 + *a;

t를 최대값을 저장하는 변수로 사용하는것을 알 수 있습니다.

1.9. a[*a]++ * 9 + *a

이 주사위 계산의 핵심입니다. 일단, *aa[0]을 의미합니다. 그래서 다시 코드를 변환 하면,

1
t = fmax(t, a[a[0]]++ * 9 + a[0]);

이렇습니다. a[0]으로 입력되는 값은 주사위 이므로 1~6입니다. a는 총 7칸 있는 배열이므로, a[0]은 입력용, 다른 a[1]~a[6]은 그동안 입력된 주사위의 개수로 사용됩니다. 주사위가 입력되면 ++연산으로 1씩 증가하는 것을 볼 수 있습니다.

그리고, a[a[0]]이 이미 값이 있다면, 그 값을 표기하기 위해 9를 곱해 이 값이 한번 이상 나타났다를 보여줍니다. 즉, t는 다음과 같은 요소를 나타냅니다.

1
t = (한 주사위가 가장 많이 등장한 횟수 - 1) x 9 + (가장 많이 등장한 주사위 중 가장 큰 주사위)

주사위 한개가 입력받을때 마다 t가 업데이트 됩니다.

여기서 보이듯, 숏코딩을 위해서라면 숏코딩 최적화된 로직을 구성해야 할 수도 있습니다.

1.10. char *에 1 더하기

printf 부분에 난데없이 덧셈 연산이 들어가 있습니다. 이는 이렇게 흘러갑니다:

  • printf에 있는 문자열을 char *에 저장
  • char* String = "1%d%0*d0" 형태로 저장된 변수
  • char* String = "1%d%0*d0" + (t<9) 덧셈 연산이 해당 방식으로 변환됨
  • (t<9)가 참이라면 1, 거짓이라면 0을 반환
  • 1을 반환했다면, char *인 포인터가 오른쪽으로 한칸 이동함. 즉, char* String = "%d%0*d0"로 변환되는것과 같음 즉, char *인 포인터에 덧셈 연산을 사용해 슬라이싱과 같은 효과를 냅니다!

1.11. printf의 포멧

printf 에서 2개의 형식 문자열이 사용 되었습니다. %d는 아주 익숙한 방식이니 넘어가도록 하고, %0*d라는 형식 문자열이 있습니다. 이 형식 문자열은 2개의 인수를 요구합니다. 출력되는 데이터의 길이를 지정하는 인수 하나와 출력 할 데이터를 지정하는 인수 하나로 구성되어 있습니다. 그래서, %0*d에는 t/90이 인수로 사용되었습니다. 즉, t/9의 길이 만큼 0을 출력한다는 뜻을 가지고 있습니다. 하지만, t/90이라도 출력은 합니다.

그래서, printft의 값에 따라 출력 방식이 변경됩니다.

  • t < 9 : 처음 1이 생략 됩니다. 가장 큰 수로 저장된 t%9를 출력 하고, t/90 이므로 0이 하나 출력됩니다.
  • 9 <= t < 18 : 처음 1이 생략되지 않습니다. 가장 큰 수로 저장된 t%d를 출력하고, t/91 이므로 0이 하나 출력됩니다.
  • 18 <= t : 처음 1이 생략되지 않습니다. 가장 큰 수로 저장된 t%d를 출력하고, t/92 이므로 0이 두개 출력됩니다.

이렇게 주사위 세개의 소스코드를 알아보았습니다.

2. 다른 테크닉들

이 외에도 C에는 숏코딩을 위한 테크닉들이 있습니다.

2.1. for문 잘 활용하기

for문은 사실 숏코딩 종합 선물 세트입니다. 무려 한줄에 3가지의 식을 실행 할 수 있기 때문이죠. 예를 들어 봅시다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
  int N;
  scanf("%d", &N);
  for(int i=1; i<=9; i++)
    printf("%d * %d = %d\n", N, i, N*i);
  return 0;
}

위와 같은 아주 정석적인 구구단 출력 코드가 있습니다. 이를 이전에 배웠던 테크닉과 for문을 활용하면…

1
main(i,N){for(scanf("%d",&N);i<=9;printf("%d * %d = %d\n",N,i++,N*i));}

이렇게 초기식은 한번 실행하는 함수, 조건식과 증감식은 조건을 판별할 때마다 반복하는 함수를 넣을 수 있습니다.

2.2. 삼항 연산자 사용하기

삼항 연산자는 if문을 대신해 줄 좋은 수단입니다.

1
main(i,N){for(i-=scanf("%d",&N);i++<N;(i%3&&i%4)?printf("%d\n",i):printf("%s%s\n",i%3?"":"Fizz",i%4?"":"Buzz"));}

FizzBuzz의 숏코딩 입니다. 다음과 같이 if문을 사용하지 않고 함수를 선택하거나, 문자열을 선택 할 수 있습니다.

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
  int N;
  scanf("%d", &N);
  printf("N is %sZero", N!=0?(N>0?"More than ":"Less than "):"");
  return 0;
}

사실 숏코딩이 아닌 일반적인 사용처에서도 효과가 있습니다.

2.3. 수학 잘하기

수학을 잘하는 것 만으로 코딩을 쉽고, 짧게 할 수 있습니다. 예를 들어 1부터 N까지의 합을 구하는 프로그램을 만든다 가정해 봅시다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
  int N, S=0;
  scanf("%d", &N);
  for(int i=1; i<=N; i++)
    S += i;
  printf("%d", S);
  return 0;
}

다음과 같이 시간 복잡도 $O(N)$의 프로그램을 짤 수도 있지만, 수학 공식을 활용해서 $O(1)$로 해결 가능합니다.

가우스 덧셈3이라고 불리는 이 공식은, $\frac{N(N+1)}{2}$으로 설명 가능하며, 1부터 $N$까지의 합을 구할 수 있습니다. 이를 이용해 코드를 작성해 볼까요?

1
main(N){scanf("%d",&N);printf("%d",N*(N+1)/2);}

이렇게 코딩에 수학을 활용한다면, 사용되는 바이트 뿐만 아니라, 시간 복잡도, 공간 복잡도까지 최적화되는 좋은 결과를 얻을 수 있습니다.

3. 마치며

숏코딩은 제목에서 서술 했듯, 흑마법 같은 존재입니다. 의도치 않은 부분에 피해를 줄 수 있고, 능률이 떨어질 수 있습니다. 하지만, 일부러 읽기 힘들게 하거나4, 순수 재미로 사악하게 활용 될 수 있기에, 언젠가 한번쯤은 해볼만한 일 인것 같습니다.

  1. EOF는 파일의 끝을 의미 하는 상수입니다. 

  2. 마찬가지로, GCC로 컴파일 하면 기본으로 불러옵니다. 

  3. 독일의 수학자 카를 프리드리히 가우스가 유년 시절 선생님으로부터 1부터 100까지의 덧셈을 해당 공식을 이용해여 아주 빠른 시간 내에 해결했다는 에피소드로 주로 알려져 있습니다. 하지만, 실제로 일어난 일인지에 대해서는 논란이 있습니다. 그리고, 이 공식의 최초 발견자로 알려진것은 기원전 5세기, 피타고라스 학파로 추정됩니다. 

  4. 리버스 엔지니어링을 어렵게 하기 위해 코드를 일부러 난독화 하는 기법이 존재합니다.