주어진 연결 리스트에서 일부분을 뒤집는 문제를 생각해보자. 연결 리스트에 [1, 2, 3, 4, 5]라는 값이 들어있고 2번째부터 4번째 사이를 뒤집는다고 하면, [1, 4, 3, 2, 5]가 된다. 연결 리스트 전체 뒤집기 문제보다 조금 더 생각할 거리가 있다.

전체 뒤집기와 다른 점은 뒤집기 경계에 있는 노드 정보를 기억해야 한다는 점이다. 뒤집기 부분 바로 직전 노드를 A라고 하고, 뒤집기 부분이 시작되는 노드를 B라고 하자. 그리고 뒤집기 부분이 끝나는 노드를 C라고 하자. 그러면 다음과 같이 처리해야 한다. 물론, 뒤집기 부분을 뒤집는 방법은 연결 리스트 전체 뒤집기와 같은 방법으로 처리해야 한다.

A.next = C
B.next = C.next

[1, 2, 3, 4, 5]의 예로 돌아가 보자. 뒤집기 부분은 [2, 3, 4]이다.  현재 노드가 가리키는 다음 노드가 현재 노드를 가리켰던 이전 노드가 되도록 하면 된다. (1) A노드는 1이므로 C노드인 4를 가리켜야 한다. (2) B노드는 2이므로 C노드의 next 노드인 5를 가리켜야 한다.

Before: 1[A] -> 2[B] -> 3 -> 4[C] -> 5
After: 1[A] -> 4[C] -> 3 -> 2[B] -> 5

개념은 위와 같다. 한 가지 주의해야 할 점은 뒤집기 부분이 첫 노드부터면 A노드가 실제로 존재하지 않고 head가 직접 C를 가리키게 해야 한다. 따라서 head가 C를 직접 가리키거나 A.next가 C가 되게 해야 한다. 이때 인자로 넘어온 head 값을 변경하기 위해서 포인터의 포인터를 사용해야 한다.

void reverseBetween(ListNode** head, int m, int n) {
	ListNode* cur = *head, *prev = *head, *start = NULL, **end = head;

	for (int i = 1; i < m; i++) {
		end = (ListNode**)&(cur->next);
		prev = cur;
		cur = cur->next;
	}
	start = cur;

	for (int i = m; i <= n; i++) {
		ListNode* nextnode = cur->next;
		cur->next = prev;
		prev = cur;
		cur = nextnode;		
	}
	start->next = cur;
	*end = prev;
}

풀기도 쉽지 않았는데, 설명하기는 더 어렵다.

'[아는게 힘이다] > [프로그래밍]' 카테고리의 다른 글

부분합(subsum) 문제 관련  (0) 2016.02.25
그래프 색칠하기  (0) 2016.02.15
[C] 몇 가지 코딩 실수  (2) 2012.05.14
[C++/ACM] 820 Internet Bandwidth  (0) 2010.05.16

댓글을 달아 주세요

정수 배열 N이 주어졌을 때 두 원소의 합이 특정한 값(T)이 되는 것을 찾는 방법에 대해 생각해보자. 예를 들면, N={-3, -5, 2, -2, 1}이고 T=-4라고 하자.

  1. 모든 경우를 따져보는 방법 - O(N^2)
  2. 해시 테이블을 사용하여 한 번만 읽으면서 찾는 방법 - O(N) (해시 테이블이 O(1)일 때)
  3. 배열을 가리키는 2개의 인덱스(혹은 포인터)를 사용하는 방법 - O(N)

이 글에서는 3번에 대해서 생각해보자. 먼저 정렬하면 N={-5, -3, -2, 1, 2}가 된다. 두 개의 인덱스(low, high)를 사용한다. low는 첫 번째 원소, high는 마지막 원소를 가리키는 인덱스를 저장한다. 각 인덱스가 가리키는 원소의 합(S)을 구해본다. 목푯값인 T와 비교하여 T<S이면 high 값을 감소시킨다. 원소의 합이 목표치보다 크기 때문에 합(S)을 줄여야 하기 때문이다. 마찬가지로, T>S이면 low 값을 증가시킨다. 원소의 합이 목표치보다 작으므로 합(S)을 키워야 하기 때문이다. 이러한 과정을 low<high를 만족하게 하는 동안 수행한다.

위의 예로 값을 구하는 과정을 살펴보면 아래와 같다.

N[low] N[high] S   
----------------------------------
-5     2       -3  (T<S, high--)
-5     1       -4  (T=S)

조금 더 문제를 확장해서 두 원소의 합이 T에 가장 가까운 경우를 찾는다고 해보자. 이 경우에도 비슷한 방식으로 하는데, S와 T의 차의 절댓값이 최소가 되는 정보를 유지해야 한다. 절댓값이라는 개념이 들어가는데도 위와 같은 식으로 하는 게 처음에는 잘 이해가 안 되는데 결국 S가 T에 가까워지는 방향으로 가려면 T>S일 떄는 low++, T<S일 때는 high--을 해야 한다.

댓글을 달아 주세요

구글 코드 잼 연습 문제를 오랜만에 풀어보았다. 사실 작년 12월 초에 풀다가 잘 안 풀려서 2달 정도 손을 놓았다가 다시 풀었다. 알고 보면 굉장히 간단한 문제인데, 접근을 엉뚱하게 해서 풀이만 복잡해지고 제대로 된 결과를 구할 수 없었다.

문제를 간단하게 서술해보자. 서로 함께 있으면 문제를 일으키는 쌍에 대한 정보를 준다. 과연 문제가 없이 사람끼리 모이도록 전체 사람들을 두 그룹으로 나누는 것이 가능한가?

접근 방법은 다음과 같다. 각 사람을 vertex로 하고, 서로 문제를 일으키는 사람 간에 edge가 존재하는 그래프를 생각한다. 그리고 각 vertex를 둘 중 하나의 색으로 칠하는데, edge로 이어진 두 vertex는 서로 다른 색이어야 한다.

처음에는 모든 vertex에 색이 없다. 칠해지지 않은 한 vertex를 선택하고, 그 vertex를 시작으로 BFS를 통해 연결된 vertex를 조사한다. 이때 이 vertex는 1) 색이 없는 상태 2) 연결될 수 없는 같은 색이 있는 상태 3) 연결될 수 있는 다른 색이 있는 상태 중 하나이다. 1)의 경우에는 다른 색을 칠하고 그것과 연결된 vertex들을 조사하게 한다. 2)의 경우 두 그룹으로 나눌 수 없다는 결론을 낸다. 3)의 경우 이 vertex에 대한 조사는 중단한다. (이미 이것과 연결된 vertex에 대한 조사는 끝났기 때문) 

BFS 과정에서 찾아지는 vertex가 없으면, 칠해지지 않고 남아 있는 vertex로 앞의 작업을 반복한다. 문제없이 모든 vertex를 칠할 수 있으면 두 그룹으로 나눌 수 있다.


댓글을 달아 주세요

회사에서 간단한 UDP 소켓 프로그래밍을 할 일이 생겼다. 입사 후 1년 8개월 정도 되었는데 기껏해야 데모를 위한 웹 페이지를 만드는 정도였다. 지난달에 안드로이드 앱이 필요해 인터넷을 뒤지며 오랜만에 Java에 손을 적셨고, 이번에 이 프로그램을 짜면서 VC++에 발을 담갔다. 그런데 몇 가지 아주 기초적인 실수로 시간을 허비해서 적어본다.

struct data {
	int a;
	int b;
};
struct data d;
char msg[32];	// buffer

//////////////////////////////////////////////////////
// ... (this part is omitted) ...
// receive data from the network to the msg variable
//////////////////////////////////////////////////////

strcpy(&d, message);

네트워크로 받은 값이 d에 제대로 복사되지 않는다.[각주:1] strcpy는 한 byte씩 복사하다가 값이 0이면 문자열의 끝이라고 생각하고 더는 값을 복사하지 않는다. 물론, strncpy를 해도 같다. 따라서 아래와 같이 memcpy를 이용해야 한다.

memcpy(&d, message, sizeof(d));

기초적인 내용인데 몇십 분을 헤맸다. 너무 오랜만에 C로 코딩해서 감이 없다.

다음은 샘플 소스에 있던 msg, strlen(msg) 부분을 보고 생각 없이 copy&paste를 해서 발생한 일이다.

send(sock, (char*)&d, strlen((char*)&d), 0);

위와 같이 하면 8bytes를 보내는 것이 아니라 d를 문자열로 봐서 8bytes 미만이 될 수도 있다. 내 경우엔 1byte가 됐다. 결국, 첫 번째 오류와 같다.

send(sock, (char*)&d, sizeof(d), 0);

이걸로 서버 프로그램이 잘못된 것이 아니냐며 관계자를 불렀다. '나는 프로그래머다'서 누가 말했듯, 자신에게 잘못이 없는지 생각하기보다 다른 사람이 잘못했을 것이라고 믿어버린 경우다. 나의 사소한 실수에 스스로 오그라들고 말았다.

프로그래밍도 감각을 잃으면 정말 사소한 곳에서 오류를 만들고 시간을 허비하게 된다. 틈틈이 프로그래밍을 게을리 말아야 겠다.

  1. 운이 좋아 d 내부의 a, b 값이 아주 크다면 제대로 값이 복사될 수도 있겠다. [본문으로]

'[아는게 힘이다] > [프로그래밍]' 카테고리의 다른 글

부분합(subsum) 문제 관련  (0) 2016.02.25
그래프 색칠하기  (0) 2016.02.15
[C++/ACM] 820 Internet Bandwidth  (0) 2010.05.16
[알고리즘] Interval Scheduling  (2) 2010.04.29

댓글을 달아 주세요


예전에는 손도 못대던 문제가 그래프 관련 문제였다. 하지만, 의외로 많은 문제들을 그래프로 풀 수 있고 재미있는 문제들도 많다. 그래서 요새는 어떤 문제가 주어지면 혹시나
그래프로
그래프로 풀 수 있는게 아닌가부터 생각한다. 이번 문제는 문제만 읽어봐도 Maximum flow 문제라는 것을 알고리즘을 공부해본 사람은 누구나 알 수 있는 문제이다. 

학부 알고리즘 수준에서 NPC를 제외하면 제일 까다로운 부분이 아닐까 생각된다. 기초적인 부분을 제외하고 이론적으로 따지고 들면 따라가기도 꽤 어렵다. 어쨌든, 이 문제는 기본적인 Max flow 알고리즘을 따라가면서 풀면 쉽게(?) 풀린다. 가장 쉬운 Ford-Fulkerson 방식이다.

간략히 알고리즘을 살펴보자.
출발 노드(s)에서 끝 노드(t)까지 flow를 보낼 수 있는 path p를 찾아서 그 p를 이루는 edge들의 residual capacity (해당 edge로 더 보낼 수 있는 flow의 최대 크기)중에서 가장 작은 c_min를 먼저 구한다. BFS 혹은 DFS를 사용하여 해당 p가 존재하는지를 알 수 있다. p의 각 edge의 flow를 c_min만큼 증가(augment)시켜준다. 이러한 작업을 계속 하다보면 더 이상 s에서 t로 flow를 보낼 수 있는 길이 없게 되는데, 그 때 s에서 나가는 flow들의 합, 혹은 t로 들어가는 flow들의 합을 구하면 그게 Max flow가 된다.

처음에 당당히 WA(Wrong answer)를 받아서 왜 그런가 생각을 해봤다. 
  1. 먼저, 마침표(.)를 안 찍은 것을 발견했다. 또 다시 WA. 
  2. 이번에는 뭔가 구현상에 문제가 있는 것이 아닌가 생각을 했다. 교과서(CLRS)에는 anti-parallel edge만 존재한다고 가정한다. 따라서, 두 노드 사이에 edge가 어느 방향으로 있는지가 꽤 중요하다. augment하려는 edge가 존재하면 c_min만큼 flow를 증가시키지만, 해당 edge가 존재하지 않으면 반대 방향으로 flow를 증가, 즉, 원래 방향으로는 flow를 감소시켜야 한다. 하지만, 이 문제에서는 항상 bi-directional한 edge가 존재하므로 이 부분을 고려하지 않아도 된다고 생각했다. 그래서 다른 사람이 짠 소스를 살펴봤더니 a->b가 c_min만큼 증가되면, b->a는 c_min만큼 감소시켰다. 나도 그렇게 바꾸고 다시 제출했다. 그런데 역시 WA. 
  3. 이번에는 문제를 다시 읽어봤다. 각 test case마다 newline이 있어야 한단다. 추가했더니 AC(accepted)
  4. 다시 2번에서 고쳤던 부분을 원래대로 돌렸다. 역시 AC. 문제는 1, 3번이었다. 2번을 적용하도 않아도 되는 것이 맞는지, 아니면 우연히 맞은 것인지는 좀 더 생각해 봐야 한다.

댓글을 달아 주세요

알고리즘 수업을 들으면 Greedy 알고리즘의 첫 번째 예로 살펴보는 것이 바로 이 interval scheduling 문제이다. 작업 목록과 각 작업을 수행할 수 있는 시간 구간이 주어진다. 그리고 최대 몇 가지 작업을 할 수 있는지를 알아내는 문제이다. 실제로 선택되는 작업들이 유일하지 않을 수도 있다. 쉬운 예를 들어보자.
  
T1: 1시-3시 청소
T2: 2시-4시 숙제
T3: 3시-5시 낮잠
T4: 4시-8시 운동
T5: 7시-9시 TV시청
T6: 8시-9시 옆집가기

위의 각각의 작업은 주어진 시간을 꽉 채워 사용해야하고 그 때에만 가능하다고 하자. 최대 다양한 작업을 하기 위해서는 1시-3시에는 청소(T1), 3시-5시에는 낮잠(T3)을 자고, 7시-9시에 TV를 보면(T5) 된다. 다른 방법(T2, T4, T6)도 하나 있지만 어쨌든, 최대 3가지 작업을 할 수 있다. 작업이 몇 개 안되면 대충 눈으로 비교해보면 쉽게 알 수 있지만 일이 100가지, 1000가지가 된다고 해보자. 쉽게 풀 수 없다. 

물론 컴퓨터를 이용하면 사람보다는 좀 더 빠르게 풀 수 있을 것이다. 일단, 무식하게 생각해서 각 작업이 결과에 포함하거나 않거나 하면 되고, 모든 경우의 수 중에서 시간이 중복되는 작업이 없는 경우의 목록의 개수가 가장 큰 경우를 찾으면 된다. 작업의 종류가 n가지라면 최대 2n가지 종류가 나온다. 대략 n=100만 되어도, log2100=30.1이므로 1030이 넘는 경우의 수가 나온다. 좀 많다. 좀 더 효과적인 방법을 고안해 내는 게 알고리즘을 연구 혹은 공부하는 사람들이 밥 먹고 하는 일이다. 그 사람들이 무엇을 발견(?)했는지 살펴보자.

일단, 가장 먼저 끝날 수 있는 작업을 선택한다. 이제 그 작업이 끝나는 시간 이후에 시작할 수 있는 작업들 중에서 가장 먼저 끝날 수 있는 작업을 택한다. 또 다시, 그 작업이 끝나는 시간 이후에 시작되는 작업 중에서 가장 빨리 끝날 수 있는 일을 택한다. 이런 식으로 택하다보면 가장 많은 작업을 택할 수 있게 된다. 프로그래밍을 하자면, 현재 시점에서 가장 빨리 끝날 수 있는 작업을 찾기 위해서는 현재 시점에서 실행 가능한 작업 목록에서 작업 시작 시간을 기준으로 정렬을 먼저 한 후 찾으면 효율적이다. 

Greedy 알고리즘은 현재 시점에서 최선의 선택을 한 것이 결과적으로 문제 전체의 최선의 선택이 된다는 것이다. 그 때 그때 Local optimal을 선택하면 결국 global optimal을 찾을 수 있다는 말이다.

다음 문제는 위 문제보다 조금 더 복잡해 보이는 문제이다. 각 작업에 가중치(weight)를 줘보자. 경제학 용어를 쓰자면 효용이라고 하면 되려나?

T1: 1시-3시 청소 - 10
T2: 2시-4시 숙제 - 50
T3: 3시-5시 낮잠 - 20
T4: 4시-8시 운동 - 20
T5: 7시-9시 TV시청 - 10
T6: 8시-9시 옆집가기 - 10

효용가치가 높은 작업 해야 하는 것이 당연하고, 주어진 시간 내에 최대 효용을 얻는 행동을 하는 것이 경제적인 인간이 마땅히 해야 할 일이다. 따라서 위 문제는 최대 효용을 얻기 위하여 어떠한 작업들을 해야 하는지에 대한 문제가 된다. 앞의 문제는 이 문제에서 모든 작업의 효용이 같다고 전제하면 동일한 문제가 된다. 그렇다면, 이 문제를 앞과 동일한 방법으로 풀 수 있을까? 안 된다. 일단 앞의 풀이처럼 T1, T3, T5를 선택하면 효용이 총 10+20+10=40이지만, T2, T4, T6을 선택하기만 해도 효용이 총 50+20+10=80이 된다. 후자를 택하는 것이 더 효율적이지만 앞의 알고리즘은 전자를 택하므로 항상 옳은 결과를 가져온다고 볼 수 없다. 

또 알고리즘을 연구하신 분들의 도움을 받자. 이번에는 greedy 알고리즘이 아니라, 또 하나의 유명한 문제 해결 기법인 dynamic programming (동적 프로그래밍)을 사용해보자. 이번에는 끝나는 시간으로 오름차순 정렬을 한다. 이 작업들을 각각 T[i]라고 하자. v[i]를 첫 번째 작업에서 부터 i번째 작업까지를 고려할 때 얻을 수 있는 최대 효용이라고 해보자. i번째 작업의 효용은 w[i]라고 하자. 

일단, v[1]은 w[1]이 된다. 이제 v[1]에서 v[i-1]이 올바른 값을 가지고 있다고 가정하자. 이제 v[i] 값은 어떤 값이 될지 생각해보자. v[i]는 (1) i번째 작업이 시작하는 시간보다 해당 작업의 끝나는 시간이 더 빠른 작업까지를 고려했을 때의 최대 효용에 w[i]를 더한 값과, (2) 그냥 i-1번째 작업까지를 고려했을 때의 최대 효용 중에 큰 값을 갖게 된다. 

왜 그렇게 될까? (1)을 살펴보자. i번째 작업의 효용 w[i]를 포함시키기 위해서는 i번째 작업과 이전의 어떤 작업도 중복되어서는 안 된다. i<j인 경우 v[i]<=v[j]가 늘 성립하므로, 그러한 값 중에 가장 큰 작업을 찾아 w[i]를 더하면 i번째 작업을 포함하는 최대의 효용이 나온다. (2)의 경우 i를 포함하지 않는 경우를 생각하는 것이다. 그렇다면 최대 효용은 i-1번째 작업까지를 고려한 경우가 된다. w[i]를 더하기 전의 (1)과 (2)는 같을 수도 있다. i를 첫 번째 작업부터 마지막 작업까지 구하면, v[n]이 바로 우리가 원하는 결과가 된다.

알고리즘은
  1. 어떤 알고리즘을 써야하는지 알기 어렵고,
  2. 그 알고리즘이 왜 옳은지 알기 어렵고,
  3. 새로운 알고리즘을 만드는 것은 더 어렵다.
남에게 설명하지 못하는 것은 이해하지 못한 것과 같다.
설명을 하려고 노력하면, 그만큼 나의 이해도도 높아지리라 믿는다.
블로그를 그런 용도로 써봐야 겠다. ㅎㅎ

댓글을 달아 주세요

문제) 주어진 문자열 배열에서 세번째로 긴 문자열 출력하라.

지난 번 폰 인터뷰에 나왔던 문제이고, 이전 포스팅인 '배열에서 두번째로 작은 수를 구하기'와 거의 유사하다. 다만, string class를 좀 제대로(?) 사용해보고자 다시 해봤다.

댓글을 달아 주세요

문제) 문자열 A가 주어지고, 그것보다 짧은 문자열로 구성된 배열 T가 주어진다. 배열 T에 들어있는 각 문자열이 A의 부분 문자열인지를 검사한다.

물론, C++에서 제공하는 string 클래스의 find 함수를 사용하면 하고 말 것도 없다. 여기에서는 좀 더 효율적이라고 생각되는 방법을 디자인해보는 것이 목표이다. 

예를 들어 A가 'apple'이라는 문자열이라고 하자. 이제, T의 임의의 문자열 t가 'apple', 'pple', 'ple', 'le', 'e'의 prefix인지 확인하면 된다. t의 첫 글자가 'a', 'p', 'l', 'e' 중의 하나가 아니라면 더 이상 비교해 볼 필요도 없다. 만일 t의 첫 글자가 'p'이면, 두 번째 글자가 'p' 혹은 'l'인지를 확인한다. A 문자열을 가지고 다음과 같은 N-ary Tree를 그리면 된다. 나의 그래프 표현 방식이 이해가 될지는 모르겠다. 왼쪽에서 오른쪽으로 보면 된다.

A+a-p-p-l-e
 +p-p-l-e
 | +l-e
 +l-e
 +e

이진 검색 트리(Binary Search Tree)는 나름 구현하기 쉽지만, N-ary tree는 구현해 본적이 없다. 자식 노드가 여러 개인 것을 어떻게 표현해야 할까 싶다. 노드의 자식 노드에 대한 포인터를 연결 리스트(Linked List) 혹은 배열로 구현해도 될 것 같다. 이번에는 C++에서 제공하는 vector를 사용해보기로 했다. 

그런데, 문제가 생겼다. 구조체 내에 vector를 선언하고 계속 데이터를 push_back을 하다 보니 메모리 위치가 중복되는지 segmentation fault가 난다. 해당 node 구조체를 malloc으로 선언할 때 그 구조체 크기보다 훨씬 크게 선언해주면 segmentation fault가 나지 않는다. 문제는 알겠지만, 해결책은 아직 구하지 못했다. 문제가 되는 라인은 32번째 라인이다.

아직 C도 아닌 것이 C++도 아닌 오묘한 코드이다.

댓글을 달아 주세요

문제) 전화기의 몇몇 번호에는 알파벳이 할당되어 있다. 전화 번호가 주어지면 가능한 알파벳 조합을 모두 출력하라. 단, 0, 1번에는 알파벳이 할당되어 있지 않고, 2~8번에 각 3개씩 알파벳이 할당되어 있다고 하자. 단 Q, Z는 할당되어 있지 않다.

일종의 순열 출력 문제라고 할 수 있다. 예를 들어 번호가 123번이면 1AD, 1AE, 1AF, 1BD, 1BE, 1BF, 1CD, 1CE, 1CF와 같이 9가지가 나온다. 기본적으로 재귀(recursion)를 사용하는 것이 생각하기 쉽다. 여기에서는 재귀 이외에도 반복(iteration)을 사용하는 것도 생각해보자.

댓글을 달아 주세요

Given
two
two strings, s1 and s2, write a program to check if s1 is a rotation of s2. But you can use strstr() only once. (abcdef = cdefab)

간단해 보이는데 방법이 잘 떠오르지 않았다. 침대에 엎드려서 생각해낸 방법으로 열심히 코딩을 한 후 답안을 확인해보니 2줄이면 끝난다. 

내가 사용한 방법은 s1의 맨 끝단어(s1[n])와 맨 앞단어(s1[0])를 붙여서 이 문자열(t)이 s2에 존재하는 위치를 먼저 구한다. t가 s2에 존재하면 t가 존재하는 위치 뒷부분, s2의 맨 앞에서 t가 존재하는 부분 직전까지의 문자열이 각각 s1에 존재하는지를 적절하게 체크한다. 다만, t가 s2에 존재하지 않는 경우 중에 s1=s2인 경우가 있으니까 주의하여 체크한다. 

 s[0]{ A }{ B }s[n] -> { B }s[n]s[0]{ A }

정답에서 사용한 방법은 s1+s1인 문자열을 만들고, s2가 그 안에 존재(strstr)하면 된다. 간단하다. 결과적으로는 s1을 순환하는 문자열로 생각한다는 점은 비슷했는데 내가 쓴 방법은 아주 지저분하고, 정답은 깔끔하다. 문제는 인터뷰를 할 때 나처럼 푼다면 짧은 시간 내에 오류 없이 소스를 완성 시키지 못할 것이 분명하기 때문에 정답과 같은 접근이 반드시 필요하다. 아흥!

댓글을 달아 주세요