https://programmers.co.kr/learn/courses/30/lessons/60063

 

코딩테스트 연습 - 블록 이동하기

[[0, 0, 0, 1, 1],[0, 0, 0, 1, 0],[0, 1, 0, 1, 1],[1, 1, 0, 0, 1],[0, 0, 0, 0, 0]] 7

programmers.co.kr

 

 

풀이

    이 문제는 DFS, BFS 모두 풀이가 가능한 문제이다. 글쓴이는 평소 DFS를 많이 이용했기 때문에 이번에는 BFS를 이용하여 풀어보았다. 로봇이 한 칸을 차지하는 것이 아닌 두 칸을 차지하므로 예외처리를 많이 해야하기 때문에 캐치하지 못한 부분이 있어 일부 테스트 케이스를 통과하지 못할 수도 있다. 나도 이 문제를 풀며 내가 생각하지 못한 부분을 예외처리하지 못하여 약간의 시간이 더 걸렸다. 로봇의 현재 위치에서 생각해야할 부분은 네 가지가 있다.

  1. 다음 위치가 보드를 벗어나는지
  2. 로봇이 차지하는 두 칸 중 하나도 1이 아닌지
  3. 이미 현재 위치를 거쳐갔는지
  4. 회전하는 경우 회전할 때 지나가는 칸이 1이 아닌지

위 네 가지만 예외처리하면 쉽게 풀 수 있는 문제이다. 예외처리만 하면 되는 문제이기 때문에 if, continue 등이 많이 쓰였다.

 

    문제를 풀 때 로봇이 상하좌우로 이동하는 경우, 회전하는 경우를 나누어서 풀었다. 상하좌우로 이동하는 경우는 일반 BFS처럼 풀면 되기 때문에 예외처리가 따로 할 부분이 없었다. 문제는 회전하는 경우였다. 회전하는 경우를 차지하는 두 칸 중에 한 칸이 대각선으로 이동하는 방식으로 생각했다. 그림으로 나타내면 다음과 같다.

위와 같이 이동한다고 할 때 실제로는 2, 4번으로 이동할 수 있지만 1, 3번으로는 이동하지 못한다. 이 부분을 예외처리하기 위해 두 칸의 x좌표나 y좌표 중 하나가 같은 경우에만 올바르게 회전했다고 판단했다. 이 과정을 다른 쪽 칸도 똑같이 진행하였다. 이렇게 회전하는 경우에 갈 수 있는 좌표의 위치를 계산할 수 있다.

 

소스 코드

#include <string>
#include <vector>
#include <queue>
#include <iostream>

using namespace std;

struct pos{ //x, y좌표를 가지는 구조체
    int x, y;
};

struct element { //큐에 넣을 구조체
    pair<pos,pos> position;
    int depth;
};

bool visit[101][101][101][101]; //방문배열

int dx[4] = {1, 0, 0, -1}; //dx, dy는 상하좌우로 이동
int dy[4] = {0, 1, -1, 0};
int rx[4] = {1, 1, -1, -1}; //rx, ry는 회전
int ry[4] = {1, -1, 1, -1};

int solution(vector<vector<int>> board) {
    int N = board.size();
    pair<pos,pos> robot = {{0, 0}, {0, 1}}; ///로봇의 현재 위치
    queue<element> q; //BFS에 사용될 큐
    
    q.push({robot, 0}); //현재 위치를 큐에 넣음
    visit[robot.first.x][robot.first.y][robot.second.x][robot.second.y] = true; //현재 위치 방문 표시
    visit[robot.second.x][robot.second.y][robot.first.x][robot.first.y] = true; //두 칸을 반대로 했을 때도 방문 표시
    
    while(!q.empty()) { //큐가 빌 때까지
        element e = q.front(); q.pop();
        pair<pos,pos> now = e.position; //현재 위치
        for(int d = 0; d < 4; d++) { //상하좌우 방향으로
            pair<pos,pos> nxt = {{now.first.x + dx[d], now.first.y + dy[d]}, {now.second.x + dx[d], now.second.y + dy[d]}}; //다음 좌표 위치
            if(nxt.first.x < 0 || nxt.first.x >= N) continue; //범위를 벗어나는 경우 건너뛰기
            if(nxt.first.y < 0 || nxt.first.y >= N) continue;
            if(nxt.second.x < 0 || nxt.second.x >= N) continue;
            if(nxt.second.y < 0 || nxt.second.y >= N) continue;
            if(board[nxt.first.x][nxt.first.y] || board[nxt.second.x][nxt.second.y]) continue; //만약 두 칸 중에 하나라도 1인 경우 건너뛰기
            if(visit[nxt.first.x][nxt.first.y][nxt.second.x][nxt.second.y]) continue; //이미 방문한 경우도 건너뛰기
            if(visit[nxt.second.x][nxt.second.y][nxt.first.x][nxt.first.y]) continue; //로봇 왼쪽 오른쪽이 반대로 됐을 경우도 같음
            if((nxt.first.x == N-1 && nxt.first.y == N-1) || (nxt.second.x == N-1 && nxt.second.y == N-1)) { //도착한 경우 방문 횟수 리턴
                return e.depth + 1;
            }
            else { //그렇지 않은 경우 다음 로봇 위치 방문 표시 후 큐에 넣기
                visit[nxt.first.x][nxt.first.y][nxt.second.x][nxt.second.y] = true;
                visit[nxt.second.x][nxt.second.y][nxt.first.x][nxt.first.y] = true;
                q.push({nxt, e.depth + 1});
            }
        }
        
        
        for(int r = 0; r < 4; r++) { //회전하는 경우
            pair<pos,pos> nxt = {{now.first.x + rx[r], now.first.y + ry[r]}, {now.second.x, now.second.y}}; //한 쪽 날개 회전 
            if(nxt.first.x != nxt.second.x && nxt.first.y != nxt.second.y) continue; //만약 한쪽을 대각선으로 회전했을 때 로봇이 차지하는 두 칸이 나란히 있지 않은 경우 건너뛰기
            if(nxt.first.x < 0 || nxt.first.x >= N) continue; //범위를 벗어나는 경우 건너뛰기
            if(nxt.first.y < 0 || nxt.first.y >= N) continue;
            if(nxt.second.x < 0 || nxt.second.x >= N) continue;
            if(nxt.second.y < 0 || nxt.second.y >= N) continue;
            if(board[now.first.x][now.first.y + ry[r]] || board[now.first.x + rx[r]][now.first.y]) continue; //회전시 지나가는 칸이 1인 경우 건너뛰기
            if(board[nxt.first.x][nxt.first.y] || board[nxt.second.x][nxt.second.y]) continue; //차지하는 두 칸중 하나라도 1인 경우 건너뛰기
            if(visit[nxt.first.x][nxt.first.y][nxt.second.x][nxt.second.y]) continue; //이미 방문한 경우 건너뛰기
            if(visit[nxt.second.x][nxt.second.y][nxt.first.x][nxt.first.y]) continue;
            
            if((nxt.first.x == N-1 && nxt.first.y == N-1) || (nxt.second.x == N-1 && nxt.second.y == N-1)) { //종료지점 도착시 리턴 
                return e.depth + 1;
            }
            else { //그렇지 않은 경우 다음 로봇 위치 방문 표시 후 큐에 넣기
                visit[nxt.first.x][nxt.first.y][nxt.second.x][nxt.second.y] = true;
                visit[nxt.second.x][nxt.second.y][nxt.first.x][nxt.first.y] = true;
                q.push({nxt, e.depth + 1});
            }
        }
            
        for(int r = 0; r < 4; r++) { //다른 한 쪽이 회전하는 경우
            pair<pos,pos> nxt = {{now.first.x, now.first.y}, {now.second.x + rx[r], now.second.y + ry[r]}}; //다른 한 쪽 날개 회전
            if(nxt.first.x != nxt.second.x && nxt.first.y != nxt.second.y) continue; //로봇이 차지하는 두 칸이 나란히 있지 않은 경우 리턴
            if(nxt.first.x < 0 || nxt.first.x >= N) continue; //범위를 벗어나는 경우 리턴
            if(nxt.first.y < 0 || nxt.first.y >= N) continue;
            if(nxt.second.x < 0 || nxt.second.x >= N) continue;
            if(nxt.second.y < 0 || nxt.second.y >= N) continue;
            if(board[now.second.x][now.second.y + ry[r]] || board[now.second.x + rx[r]][now.second.y]) continue; //회전시 지나가는 칸이 1인 경우 건너뛰기
            if(board[nxt.first.x][nxt.first.y] || board[nxt.second.x][nxt.second.y]) continue; //차지하는 두 칸중 하나라도 1인 경우 건너뛰기
            if(visit[nxt.first.x][nxt.first.y][nxt.second.x][nxt.second.y]) continue;//이미 방문한 경우 건너뛰기
            if(visit[nxt.second.x][nxt.second.y][nxt.first.x][nxt.first.y]) continue;
            
            if((nxt.first.x == N-1 && nxt.first.y == N-1) || (nxt.second.x == N-1 && nxt.second.y == N-1)) { //종료지점 도착시 리턴
                return e.depth + 1;
            }
            else { //그렇지 않은 경우 다음 로봇 위치 방문 표시 후 큐에 넣기
                visit[nxt.first.x][nxt.first.y][nxt.second.x][nxt.second.y] = true;
                visit[nxt.second.x][nxt.second.y][nxt.first.x][nxt.first.y] = true;
                q.push({nxt, e.depth + 1});
            }
        }
    }
}

같은 코드가 반복되어 비효율적인 부분이 있긴 하지만 프로그램 자체가 길지 않으므로 하나의 함수에 작성하였다.시간의 효율성을 생각해보면 큐 안의 반복문이 4번 반복하는 for문이 3개 있으므로 배열의 크기를 N이라고 할 때 약 N * N * 12번의 반복이 발생하지만 12번의 반복은 10^8이 약 1초 걸린다고 생각할 때 무시해도 될 정도의 아주 작은 시간이므로 그다지 효율성을 떨어뜨리지 않는다고 할 수 있다.

 

채점 결과

https://www.acmicpc.net/problem/2565

 

2565번: 전깃줄

첫째 줄에는 두 전봇대 사이의 전깃줄의 개수가 주어진다. 전깃줄의 개수는 100 이하의 자연수이다. 둘째 줄부터 한 줄에 하나씩 전깃줄이 A전봇대와 연결되는 위치의 번호와 B전봇대와 연결되는

www.acmicpc.net

 

 

풀이

이 문제는 LIS를 이용해 풀면 쉽게 풀 수 있는 문제이다. 우선 왼쪽 전봇대에 연결된 위치 순서의 오름차순으로 정렬한다. 하나의 위치에 하나의 전깃줄만 연결되어 있으므로 중복되는 번호는 존재하지 않는다. 그렇다면 이제부터 오른쪽 전봇대에 연결된 위치만 생각하면 된다. 오른쪽 전봇대에 연결된 위치의 번호의 LIS의 최대 길이가 전깃줄을 최소로 제거하는 방법이다. 그림으로 보면 아래와 같다. 

파란색 선이 꼬이지 않고 최대로 연결한 방법이고 빨간 선이 삭제해야하는 전깃줄이다. 번호를 보면 왼쪽 번호가 증가함에 따라 오른쪽 번호도 당연히 증가하는 것을 알 수 있다. 만약 오른쪽 번호가 증가하다 감소하면 전깃줄이 꼬였다는 이야기이다. 따라서 이 문제는 왼쪽 전봇대의 번호 순으로 정렬한 다음 오른쪽 전깃줄 번호의 가장 긴 증가하는 부분 수열의 길이를 구하는 문제이다. 코드는 다음과 같다.

 

소스 코드

#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>

using namespace std;
vector<pair<int,int>> arr;
vector<int> dp;

bool cmp(pair<int,int> a, pair<int,int> b) { //왼쪽 전봇대 번호 정렬
	return a.first < b.first;
}

int main() {
	int N; cin >> N;
	for(int i = 0; i < N; i++) {
		int a, b; cin >> a >> b;
		arr.push_back({a,b});
	}
	sort(arr.begin(), arr.end(), cmp); //정렬
	dp.push_back(-1); //초기값 설정
	for(int i = 0; i < N; i++) { //LIS 최대 길이 구하기
		if(dp.back() < arr[i].second) { //가장 큰 원소보다 큰 경우 뒤에 push
			dp.push_back(arr[i].second);
		}
		else { //그렇지 않은 경우 이진탐색을 이용하여 알맞은 위치에 교체
			auto it = lower_bound(dp.begin(), dp.end(), arr[i].second);
			*it = arr[i].second;
		}
	}
	cout << N - (dp.size()-1); //총 개수 N에서 최대 LIS 길이를 뺀 수만큼 전깃줄 제거해야함
}

 

채점 결과

 

리눅스에서는 현재 사용자가 누구인지를 나타내는 방법이 바로 사용자 아이디이다. 하지만 프로그래밍을 하다보면 자신이 아닌 다른 사람이 만든 파일이나 프로그램을 사용하고 싶거나 내용을 바꿔야 할 때가 있다. 이런 상황을 위해 리눅스에서는 세 가지 ID를 사용한다.

 

사용자 아이디 종류

 

1. 실제 사용자 ID(Real User ID) : 프로그램을 실제 수행시키는 사용자에게 속한 유효 사용자 ID

  • 프로세스를 실행한 사용자의 ID이다.
  • 실제 사용자 ID는 부모 프로세스의 실제 사용자 ID로 설정된다.
  • exec() 호출 도중에 변경되지 않는다.
  • 루트 권한을 갖는 프로세스만 실제 사용자 ID를 다른 값으로 변경할 수 있다.
  • 루트 권한을 제외하고는 생성하는 자식 프로세스들은 같은 RUID를 가진다.

2. 유효 사용자 ID(Effective User ID) : 프로세스 실행 자격을 확인하는 사용자 ID

  • 프로세스가 영향을 미치는 사용자 ID이며 접근 권한을 판단할 때 사용한다.
  • 프로세스를 처음 생성할 때는 부모 프로세스의 유효 사용자 ID를 상속받는다. 따라서 EUID와 RUID가 같다.
  • exec()함수를 호출할 때에도 일반적으로 RUID는 변경되지 않는다.
  • 프로세스는 유효 사용자 ID를 변경 가능하다. -> setuid bit가 설정되어 있는 프로그램을 실행할 때만 가능하다.

3. 저장된 사용자 ID(Saved User ID) : exec()호출 시 변경되기 전 유효 사용자 ID

  • 프로세스의 원래 유효 사용자 ID
  • 프로세스가 fork()를 실행할 때 자식 프로세스는 부모 프로세스의 저장된 사용자 ID를 상속받는다. 즉 부모 프로세스의 바뀐 유효 사용자 ID가 아니라 원래 유효 사용자 ID이다.
  • exec()도 동일하게 커널은 저장된 사용자 ID를 유효 사용자 ID로 설정한다.
  • 프로그램의 사용자 ID가 유효 사용자 ID에 복사될 때 이전 유효 사용자 ID는 저장된 사용자 ID에 복사된다. 그림으로 나타내면 아래와 같다.
  • 루트 권한을 갖는 프로세스만 실제 사용자 ID와 동일한 값으로 변경 가능하다.

setuid bit가 설정된 프로그램을 실행할 경우의 RUID, EUID, SUID의 변경 과정

실제 예시

/usr/bin/passwd 파일은 패스워드를 변경하는 프로그램이고 소유자는 root이다.

패스워드가 들어있는 파일은 다른 파일에 존재하고 소유자는 root이다. 그리고 소유자만 이 파일을 읽고 쓸 수 있다.

/usr/bin/passwd 파일은 setuid설정된 파일이다.

소유자는 root이므로 일반 사용자가 이 프로세스(/usr/bin/passwd)를 실행할 경우 일반 사용자의 EUID는 root가 된다. 따라서 일반 사용자도 EUID가 root이므로 패스워드가 들어있는 파일의 접근 권한과 같으므로 패스워드가 들어있는 파일에 읽고 쓸 수 있다.

즉 일반 사용자는 패스워드가 들어있는 파일에 직접 읽고 쓸 수 있는 것이 아니라 /usr/bin/passwd라는 패스워드를 변경하는 프로그램을 이용해 패스워드가 들어있는 파일에 읽고 쓸 수 있는 권한을 얻어 변경할 수 있는 것이다.

Exec함수란?

    이전 글(https://shyeon.tistory.com/51)에서 fork, vfork를 이용하여 자식 프로세스를 생성하여 다중 프로세스를 이용하는 프로그램을 만드는 법을 알아보았다. vfork를 이용할 경우 부모 프로세스는 자식 프로세스를 기다리는 것을 보장하고 주로 exec 계열 함수를 호출할 경우 사용한다고 했다. 그렇다면 exec 계열 함수는 어떤 기능을 수행할까?

 

    exec 계열 함수는 현재 실행되고 있는 프로세스를 다른 프로세스로 대신하여 새로운 프로세스를 실행하는 함수이다. 즉 다른 프로세스로 대체되어 주어진 경로에 있는 새로운 프로세스의 main함수부터 다시 시작된다. 

 

함수

int execl(const char *pathname, const char *arg0, ... /* (char *)0*/);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../*(char *)0, char *const envp[] */);
int execve(const char *pathname,  char *const argv[], char *const envp[]);
int execlp(const char *filename, const c har *arg0, .../* (char *)0 */);
int execvp(const char *filename, char *const argv[]);

6개의 함수들이 전부 비슷하게 생겼다. 하지만 규칙을 찾을 수 있다.

  1. 뒤에 l 이 붙은 경우 : 매개변수의 인자가 list 형태이다. 즉 char*형을 나열한다.
  2. 뒤에 v가 붙은 경우 : 매개변수의 인자가 vector 형태이다. 두 번째 인잘르 보면 2차원 배열로 char*형을 배열로 한 번에 넘긴다.
  3. 뒤에 e가 붙은 경우 : 환경변수를 포함하여 넘긴다. 
  4. 뒤에 p가 붙은 경우 : 경로 정보가 없는 실행 파일 이름이다. 만약 filename에 ' / '(슬래시)가 포함되어 있으면 filename을 경로로 취급하고 슬래시가 없으면 path로 취급한다.

list 형태와 vector 형태로 넘기는 매개변수의 맨 마지막은 NULL pointer가 존재해야한다.

위 함수중 execve() 함수만 커널에서 제공하는 system call 함수이고 나머지는 라이브러리 함수이다.

 

exec 계열 함수를 호출한 프로세스에서 대체되는 프로세스로 상속되는 특징

  • 프로세스 ID와 부모 프로세스 ID
  • 실제 사용자 ID와 실제 그룹 ID
  • 추가 그룹 ID
  • 프로세스 그룹 ID
  • 세션 ID
  • 제어 터미널
  • alarm 발동까지 남은 시간
  • 현재 작업 디렉토리
  • 파일 생성 마스크
  • 파일 자물쇠들
  • 프로세스 시그널 마스크
  • 아직 처리되지 않은 시그널
  • 자원 한계
  • tms_utime, tms_stime, tms_cutime, tms_cstime

 

예제

이번 예제는 두개의 프로그램을 짜서 하나의 프로그램에서 exec함수를 실행하여 다른 프로그램으로 대체하는 프로그램을 짜보겠다.

 

1. exec1.c(exec을 호출하여 exec2를 실행하는 프로그램)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
	pid_t pid;

	if((pid = fork()) == 0) { //자식이 실행
		char *args[] = {"hello", "my", "name", "is", "shyeon", NULL}; //exec호출시 넘길 인자들
		if(execv("./exec2", args) < 0) { //exec2를 실행한다.
			fprintf(stderr, "execv error\n");
			exit(1);
		}
	}
	if(wait(NULL) == pid) { //자식이 종료되길 기다림
		printf("\nchild process exited\n");
	}
	else { //에러시 에러문구 출력
		fprintf(stderr, "wait error\n");
		exit(1);
	}

	exit(0);
}

 

2. exec2.c(exec1으로부터 실행되는 프로그램)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv) {

	printf("parent says : ");

	for(int i = 1; i < argc; i++) { //부모에서 넘긴 인자를 출력
		printf("%s ", argv[i]);
	}

	exit(0);
}

이렇게 exec1에서 exec2로 대체한 것처럼 하나의 프로그램에서 다른 프로그램을 호출하며 프로그램을 대체할 수 있다.

 

실행 결과

위 프로그램에 대한 실행 결과이다.

exec1을 실행해서 execv를 통해 exec2가 실행되는 것을 알 수 있다.

https://www.acmicpc.net/problem/12015

 

12015번: 가장 긴 증가하는 부분 수열 2

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000,000)

www.acmicpc.net

 

풀이

    이 문제는 이전 글에서 다루었던 가장 긴 증가하는 부분 수열(https://shyeon.tistory.com/52)에서 사용한 DP를 이용할 경우 시간 복잡도가 O(n^2)이  걸리므로 이 문제의 최대 길이인 10^6이 주어질 경우 10^12으로 시간제한인 1초를 훌쩍 넘게 된다. 따라서 이 문제는 O(N * log N)이 걸리는 알고리즘 방식을 이용해야한다.

    이 알고리즘은 가장 현재까지 탐색한 수열 중 가장 긴 수열의 길이를 가진 배열을 항상 유지한다. 만약 배열의 맨 마지막 원소 즉 가장 큰 값보다 클 경우 배열 맨 뒤에 붙여준다. 그렇지 않을 경우 현재 위치의 원소가 그 배열에서 위치할 수 있는 곳을 이진탐색을 이용하여 찾고 그 위치의 원소와 바꾼다. 예시를 이용하여 다시 설명하겠다.

 

예제

 

위 예제를 풀이한 알고리즘 순서대로 풀어보겠다. 처음 원소는 음수의 무한대로 초기화한다. 따라서 

  1. -INF
  2. -INF 10 (맨 끝에 추가)
  3. -INF 10 20 (맨 끝에 추가)
  4. -INF 10 20 (10자리에 10이 들어갈 수 있음)
  5. -INF 10 20 30 (맨 끝에 추가)
  6. -INF 10 20 30 (20 자리에 10 교체)
  7. -INF 10 20 30 50 (맨 끝에 추가)

위의 예시는 반복되는 숫자가 있으므로 다른 예시로 한 번 더 해보겠다.

 

예시 : 10 20 40 30 70 90 50 60 80

  1. -INF
  2. -INF 10
  3. -INF 10 20
  4. -INF 10 20 40
  5. -INF 10 30 40
  6. -INF 10 30 40 70
  7. -INF 10 30 40 70 90
  8. -INF 10 30 40 50 90
  9. -INF 10 30 40 50 60
  10. -INF 10 30 40 50 60 80

이런 식으로 진행된다. 따라서 마지막 크기에서 -1을 해주면 가장 긴 수열의 길이를 구할 수 있다. 마지막에는 가장 긴 수열이 저장되는 것은 아니지만 길이는 이 방법을 이용하여 O(N log N)으로 구할 수 있다.

 

소스 코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int MN = 1001;
const int INF = 1e9; //무한대

vector<int> dp;

int main() {
    int N; cin >> N;
    dp.push_back(-INF); //마이너스 무한대 push
    for(int i = 0; i < N; i++) {
        int a; cin >> a; //입력 받아서
        if(dp.back() < a) { //배열의 맨 마지막 원소보다 크면 뒤에 저장
            dp.push_back(a);
        }
        else { //그렇지 않을 경우
            auto it = lower_bound(dp.begin(), dp.end(), a); // 이진탐색을 이용하여 들어갈 수 있는 위치 탐색
            *it = a; //해당 위치에 교체
        }
    }

    cout << dp.size() - 1; // -무한대 빼고 길이 출력
}

 

채점 결과

 

https://www.acmicpc.net/problem/11053

 

11053번: 가장 긴 증가하는 부분 수열

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이

www.acmicpc.net

 

풀이

    문제는 간단하다 수열이 주어졌을 때 수열의 순서를 바꾸지 않고 증가하는 부분 수열로 가장 긴 길이를 출력하는 문제이다.

이 문제는 DP를 이용하여 풀 수 있다. 이 알고리즘을 DP를 이용하여 해결하는 데에는 O(n^2)의 시간 복잡도를 가진다. 브루트 포스 방식으로 i번째 인덱스부터 끝까지 증가하는 수열의 길이를 찾고 가장 긴 길이를 출력하면 된다.

 

소스 코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int MN = 1001;

int dp[MN];
int arr[MN];

int main() {
    int N; cin >> N; //길이 입력
    int res = 0; //최대 길이 저장

    for(int i = 0; i < N; i++) //수열 입력
        cin >> arr[i];

    for(int i = 0; i < N; i++) { //i번째 인덱스까지
        dp[i] = 1; //원소 1개의 길이는 1
        for(int j = 0; j < i; j++) { //i번째 인덱스까지
            if(arr[i] > arr[j]) { //증가하는 수열이면
                dp[i] = max(dp[i], dp[j] + 1); //i번째 인덱스까지 최대길이 갱신
            }
        }
        res = max(res, dp[i]); //최대 길이 저장
    }

    cout <<res;
}

 

이 문제는 dp를 이용하여 i번째 인덱스까지 만들어지는 수열 중 증가하는 수열의 최대 길이만 갱신하는 방식이다. 이 방법의 아쉬운 점은 가장 긴 증가하는 부분 수열 자체를 구할 수는 없다. 또 이 알고리즘은 O(n^2)의 시간 복잡도를 가지는 것이 굉장히 아쉬운 부분이다. 다른 방식을 사용하면 O(N * log N)으로 해결할 수 있다. 이 방식은 가장 긴 증가하는 부분 수열 2에서 다루도록 하겠다.

프로세스란?

  • 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램을 말한다.
  • 우리가 코드를 짜서 만든 프로그램을 컴파일하여 실행시키면 실행되고 있는 프로그램은 하나의 프로세스가 된다.
  • 우리가 일반적으로 짜는 프로그램은 단일 프로세스의 단일 스레드 프로그램이다.
  • 만약 한 프로세스에서 다른 프로세스를 생성할 경우 생성된 프로세스를 자식 프로세스라 부르고 생성한 프로세스를 부모 프로세스라 부른다.

 

부모 프로세스로부터 상속받는 속성

  • SUID 플래그와 SGID 플래그
  • 현재 작업하고 있는 디렉토리
  • 루트 디렉토리
  • 파일 생성 마스크
  • 시그널 마스크와 시그널 처리 설정
  • 열린 file discriptor에 대하여 exec 호출 시 닫기(close-on-exec) 플래그
  • 환경변수
  • 부착된 공유 메모리 영역
  • 메모리 매핑
  • 자원 한계

 

부모 프로세르로부터 상속받지 않는 속성

  • fork의 리턴값(부모는 0이 아닌 값, 자식은 0)
  • 프로세스 ID
  • 부모 프로세스 ID(생성된 프로세스는 자신을 생성한 프로세스의 ID가 부모 프로세스 ID로 설정된다.
  • 자식 프로세스의 tims_utime(user코드를 실행하는데 사용된 cpu 시간), tms_stime(커널 코드를 실행하는데 사용된 cpu 시간), tms_cutime(child user 시간), tms_cstime(child의 커널 시간)은 모두 0으로 설정
  • 부모가 잠근 파일 lock
  • 아직 발동되지 않은 alarm 시그널은 자식에서 모두 해제
  • 자식의 유보 중인 시그널 집합

 

함수

1. fork()

pid_t fork(void);
  • 새로운 자식 프로세스를 생성할 때 사용하는 시스템 호출 함수이다.
  • 한 번 호출에 두 개의 리턴 값을 리턴한다. 자식 프로세스에게는 , 부모에게는 자식 프로세스의 ID를 리턴한다.
  • 프로세스 생성이 실패한 경우 -1을 리턴한다.

2. vfork()

pid_t vfork(void);
  • fork()와 마찬가지로 자식 프로세스를 생성하는 함수이다.
  • fork와의 차이점은 자식 프로세스가 먼저 실행됨을 보장한다는 것 이다. 따라서 생성된 프로세스가 exec계열 함수를 이용하여 새 프로그램으로 실행하는 경우에 주로 사용한다.
  • vfork로 프로세스를 생성한 경우 exec계열 함수를 호출할 것으로 생각하므로 부모의 주소 영역을 참조하지 않을 것이라고 생각하여 부모 프로세스의 공간을 자식에게 복사하지 않는다.
  • 복사하는 시간이 소요되지 않으므로 fork보다 약간의 성능 향상이 있다.
  • 생성된 자식 프로세스는 exec계열 함수나 exit()을 호출할 때까지 부모 프로세스의 메모리 영역에서 실행되고 부모 프로세스는 자식 프로세스가 exec계열 함수나 exit()을 호출할 때까지 기다린다.
  • 자식 프로세스가 좀비 프로세스가 되는 것을 최대한 방지하고 자식 프로세스가 항상 먼저 실행되는 것을 보장한다.
  • 자식 프로세스를 종료할 때 _exit()을 이용하여야한다. 부모 프로세스의 표준 입출력 채널을 같이 사용하는데 자식 프로세스가 exit()을 호출할 경우 입출력 스트림을 모두 닫으므로 부모 프로세스에서는 입출력을 하지 못한다. 따라서 입출력 스트림을 정리하지 않고 종료시키는 _exit()을 사용해야 한다.

3. wait(), waitpid()

pid_wait(int *statloc);
pid_waitpid(pid_t pid, int *statloc, int options);
  • 프로세스의 종료 상태를 회수하는 system call 함수이다.
  • 자식 프로세스가 종료될 때까지 부모 프로세스가 기다리는 함수이다.
  • 자식 프로세스가 언제 종료하는지 부모 프로세스는 알 지 못하므로 커널에서 시그널(SIGCHLD)을 보내 부모 프로세스에게 종료 사실을 알린다.
  • 부모 프로세스는 시그널을 무시할 수도 있고 핸들러 함수를 등록하여 핸들러에게 규정된 동작을 수행하게 할 수 있다.
  • statloc이 가리키는 곳에는 자식 프로세스의 종료 상태를 저장한다. 만약 자식 프로세스가 존재하지 않을 경우 -1을 리턴한다.
  • 리턴 값은 종료된 프로세스의 pid값이다.
  • waitpid의 pid값에 따라 기다리는 자식 프로세스가 달라진다. 
    1. -1인 경우 임의의 자식 프로세스를 기다리며 wait()과 동일한 역할을 수행한다.
    2. 0보다 큰 경우 프로세스 id가 pid인 하나의 자식 프로세스를 기다린다.
    3. 0인 경우 프로세스 그룹 id가 호출한 프로세스와 동일한 임의의 자식 프로세스를 기다린다.
    4. -1보다 작은 경우 프로세스 그룹 id가 pid의 절대값과 같은 임의의 자식 프로세스를 기다린다.
  • wait()은 가장 먼저 종료된 자식 프로세스 하나만을 기다린다. 만약 자식 프로세스가 없는 경우 에러를 리턴한다. 종료된 프로세스가 없는 경우 부모 프로세스는 자식 프로세스를 기다린다.

 

예제

자식 프로세스를 생성하여 자식 프로세스에서 1부터 10까지 출력하고 부모는 자식프로세스를 기다린 다음 1부터 10까지 출력하는 프로그램이다.

 

소스 코드

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
	pid_t pid;
	
	if((pid = fork()) < 0) { //에러인 경우
		fprintf(stderr, "fork error\n");
		exit(1);
	}
	else if(pid == 0) { //이 괄호 안은 자식이 실행
		for(int i = 1; i <= 10; i++) {
			printf("child : %d\n", i);
		}
		exit(0);
	}
	else { //여기부터 부모가 실행
		if(waitpid(pid, NULL, 0) < 0) { //자식 프로세스가 끝날 때까지 기다림
			fprintf(stderr, "wait pid error\n");
			exit(1);
		}
		for(int i = 1; i <= 10; i++) { //부모 프로세스에서 1~10까지 출력
			printf("parent : %d\n", i);
		}
	}
	exit(0);
}

 

실행 결과

조건 변수를 사용하는 이유

    이전 글(https://shyeon.tistory.com/48)에서 mutex를 이용하여 스레드간 동기화를 하는 방법을 알아 보았다. 하지만 mutex만을 이용하여 동기화를 하는 것은 프로세스가 mutex변수를 지속적으로 검사해야 한다는 단점이 있다. 따라서 이 글에서 다루는 조건 변수(condition)을 함께 사용한다. 

 

함수

함수는 mutex함수와 굉장히 유사하다.

1. pthread_cond_init

int pthread_cond_init(pthread_cond_t *restrict condition, pthread_condattr_t *restrict attr);

mutex변수를 초기화하는 것처럼 cond변수도 초기화를 해야한다. 첫 번째 인자는 초기화하려는 cond 변수의 주소값을 넘기고 두 번째 인자는 cond 변수의 속성을 설정하는 변수이고 리눅스에선 무시하므로 NULL값을 주로 넣는다.

mutex 변수처럼 pthread_cond_init함수 말고 직점 변수에 PTHREAD_COND_INITIALIZER를 대입하여 초기화하는 방법도 있다.

 

2. pthread_cond_destroy

int pthread_cond_destroy(pthread_cond_t *condition);

mutex 변수와 마찬가지로 사용이 끝난 cond 변수는 pthread_cond_destroy함수를 호출하여 해제해줘야 한다.

 

3. pthread_cond_signal

int pthread_cond_signal(pthread_cond_t *cond);

cond 변수에 신호를 보내어 cond 변수를 기다리고 있는 스레드를 다시 시작시키는 함수이다. 만약 cond 변수를 기다리는 스레드가 없다면 아무 일도 일어나지 않느다. pthread_cond_signal함수는 cond 변수를 기다리는 여러 스레드 중에 단 하나만 다시 시작시킨다. 하지만 어떤 스레드를 시작시킬지는 지정할 수 없다.

 

4. pthread_cond_broadcast

int pthread_cond_broadcast(pthread_cond_t *cond);

하나의 스레드만 다시 시작시키는 pthread_cond_signal 함수와 다르게 cond 변수를 기다리고 있는 모든 스레드를 다시 시작시킨다.

 

5. pthread_cond_wait

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

cond 변수가 신호를 받을 때까지 기다리는 함수이다. 이 함수가 실행되면 mutex를 lock을 해제하고 기다린다. 따라서 다른 스레드에서는 mutex변수의 lock을 얻을 수 있다. 그 다음 이 함수를 벗어날 경우 mutex lock을 얻게 된다. 기다리는 시간을 지정할 수 있는 pthread_cond_timedwait도 있다.

 

예제

이 예제는 1부터 10까지의 숫자를 두 개의 스레드에서 출력한다. 1~3은 1번 스레드가, 4~7은 2번 스레드가, 8~10은 다시 1번 스레드가 실행한다. 1번 스레드가 실행할 부분은 2번 스레드에서 신호를 주어 cond변수를 이용하여 다시시작한다.

 

소스 코드

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //변수 초기화
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int value = 0; //공유 변수

void *func1(void *arg) { //스레드1이 실행할 함수
	while(1) {
		pthread_mutex_lock(&mutex); //lock을 걸고
		pthread_cond_wait(&cond, &mutex); //기다림
		value++; //1 증가시키고
		printf("thread1 : %d\n", value); //출력

		pthread_mutex_unlock(&mutex); //lock 해제

		if(value >= 10) //10을 넘어갈 경우 종료
			return NULL;
	}
}

void *func2(void *arg) { //스레드2가 실행할 함수
	while(1) {
		pthread_mutex_lock(&mutex); //lock을 걸고

		if(value < 3 || value > 6) { //1번 스레드가 실행할 부분
			pthread_cond_signal(&cond); //cond변수를 기다리는 스레드 1번을 다시 시작시킴
		}
		else { //그 외의 부분은
			value++; //1 증가시키고
			printf("thread2 : %d\n", value); //출력
		}

		pthread_mutex_unlock(&mutex); //lock 해제

		if(value >= 10)
			return NULL;
	}
}

int main() {
	pthread_t tid1, tid2;

	pthread_create(&tid1, NULL, &func1, NULL); //스레드 1 생성
	pthread_create(&tid2, NULL, &func2, NULL); //스레드 2 생성

	pthread_join(tid1, NULL); //스레드1 리소스 반환
	pthread_join(tid2, NULL); //스레드2 리소스 반환

	pthread_mutex_destroy(&mutex); //mutex 변수 해제
	pthread_cond_destroy(&cond); //cond 변수 해제

	return 0;
}

 

실행 결과

1~3, 8~9는 1번 스레드가 출력하고 4~7은 2번 스레드가 출력하는 것을 볼 수 있다.

+ Recent posts