프로그래밍의 흑마법: 숏코딩을 C로 알아보기
코드 골프라고도 불리는 숏코딩은, 코드를 최적화나 가독성을 고려하지 않고, 최대한 적은 바이트로 코드를 작성하는 일종의 코딩 스타일 입니다. 해외에서는 이 숏코딩을 일종의 대회나 경쟁 요소로도 활용됩니다. 국내에서도 알고리즘 문제 해결 사이트인 백준 온라인 저지에서 문제 해결을 위해 사용한 바이트 수를 가지고 순위를 매기기도 합니다. 오늘은 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 argc
는 main
함수를 호출 할때 사용된 파라미터의 개수, char **argv
는 호출시 사용된 파라미터, char **env
는 환경 변수입니다. 하지만, 이를 모두 무시하고 변수를 사용 할 수 있습니다. 하지만, 첫번째 변수 외에는 쓰레기 값이 들어오므로, 초기화가 필요합니다.
1.6. scanf
와 ~
scanf
는 함수 실행 후 반환값으로 성공적으로 읽은 인자를 반환합니다. scanf("%d", a)
에서 a
에 1
이 들어왔으면, 반환값으로 1
이 반환되는 방식입니다. 하지만, 읽기 오류가 발생한다면 EOF
1를 반환합니다. 그리고 이 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
?
fmax
는 math.h
2에 포함된 함수중 하나로, double
형 변수 두개를 비교하여 더 높은 변수를 반환합니다. int형도 마찬가지로 변수 형 확장을 거친 후, 정상적으로 사용됩니다.
다음 코드와 같은 의미를 가집니다.
1
2
if(t < a[*a]++ * 9 + *a)
t = a[*a]++ * 9 + *a;
t
를 최대값을 저장하는 변수로 사용하는것을 알 수 있습니다.
1.9. a[*a]++ * 9 + *a
이 주사위 계산의 핵심입니다. 일단, *a
는 a[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/9
와 0
이 인수로 사용되었습니다. 즉, t/9
의 길이 만큼 0
을 출력한다는 뜻을 가지고 있습니다. 하지만, t/9
가 0
이라도 출력은 합니다.
그래서, printf
는 t
의 값에 따라 출력 방식이 변경됩니다.
- t < 9 : 처음
1
이 생략 됩니다. 가장 큰 수로 저장된t%9
를 출력 하고,t/9
가0
이므로0
이 하나 출력됩니다. - 9 <= t < 18 : 처음
1
이 생략되지 않습니다. 가장 큰 수로 저장된t%d
를 출력하고,t/9
가1
이므로0
이 하나 출력됩니다. - 18 <= t : 처음
1
이 생략되지 않습니다. 가장 큰 수로 저장된t%d
를 출력하고,t/9
가2
이므로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, 순수 재미로 사악하게 활용 될 수 있기에, 언젠가 한번쯤은 해볼만한 일 인것 같습니다.