14499번 주사위 굴리기

 

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

 

14499번: 주사위 굴리기

첫째 줄에 지도의 세로 크기 N, 가로 크기 M (1 ≤ N, M ≤ 20), 주사위를 놓은 곳의 좌표 x y(0 ≤ x ≤ N-1, 0 ≤ y ≤ M-1), 그리고 명령의 개수 K (1 ≤ K ≤ 1,000)가 주어진다. 둘째 줄부터 N개의 줄에 지도에 쓰여 있는 수가 북쪽부터 남쪽으로, 각 줄은 서쪽부터 동쪽 순서대로 주어진다. 주사위를 놓은 칸에 쓰여 있는 수는 항상 0이다. 지도의 각 칸에 쓰여 있는 수는 10을 넘지 않는 자연수이다. 마지막 줄에

www.acmicpc.net

문제 해석


 

 

 

 

어떤 판 위에 주사위가 있습니다. 이 판에는 임의의 숫자가 적혀있습니다. 주사위를 입력받은 방향으로 굴리면서 주사위 윗면의 숫자를 조건에 따라 출력해주는 문제입니다. 규칙을 따져보자면 이렇습니다.

 

1. 처음 주사위의 모든 면에는 0이 쓰여있습니다. 그러니까 처음 6면에는 모두 0인 수가 적혀있는 것이죠.

2. 주사위가 굴러가면서 주사위 아랫면에 판의 숫자가 쓰여집니다. 단, 판의 숫자가 0일때는 주사위 윗면의 숫자를 출력해줍니다.

3. 만약 주사위가 판의 범위에서 나가는 입력이 들어왔을때는 깔끔하게 무시해주면 됩니다.

 

 

제약 조건


판의 가로(N), 세로(M) 크기는 1이상 20이하, 그리고 주사위를 어느 방향으로 굴릴지에 대한 입력은 1이상 1000이하로 들어옵니다. 주사위의 처음 위치는 판의 범위에 있어야하므로 좌표의 위치(x,y)는 0 ≤ x ≤ N-1, 0 ≤ y ≤ M-1 입니다.

 

풀이


 

문제의 조건이 약간 이상하긴 합니다. 보통 x는 가로, y는 세로를 문제로 주고 역시 n도 세로의 길이, m도 가로의 길이로 주는게 조금 일반적이거든요.

그래서 저는 y를 세로 좌표, x를 가로 좌표로 두고 문제를 풀겠습니다.

 

우선 주사위를 어떻게 굴릴지 머리부터 굴려봅시다. 우선 전개도로 어떻게 주사위를 굴려볼까 봅시다. 

그림에서 가장 가운데 주사위의 전개도가 초기의 주사위입니다. 분홍색이 아랫면, 주황색이 윗면이라고 가정하지요. 주사위의 왼쪽면은 왼쪽 날개, 오른쪽 면은 오른쪽 날개라고 가정한다면 가운데의 주사위 전개도의 모든 면은 이렇게 되겠네요.

0번: 뒷면, 1번: 아랫면, 2번: 앞면, 3번: 윗면, 4번: 왼쪽면, 5번: 오른쪽면

그렇죠?

우리는 이것을 배열 인덱스로 생각해야합니다. 그러니 전개도의 숫자는 적혀져있는 숫자가 아니고 자리를 나타냅니다.

 

 

 

 

만약 이 상태에서 오른쪽으로 주사위를 굴린다면 0번 자리에 있던 수는 그대로 0번 자리에 있던 수가, 기존 1의 위치에는 5번의 자리에 있는 수가, 기존 2의 자리에는 그대로 2의 자리에 있던 수가, 기존 3의 자리에는 4의 자리에 있던 수가, 기존 4의 자리에는 1의 자리에 있던 수가, 기존 5의 자리에는 3의 자리에 있던 수가 됩니다. 숫자들이 자리를 찾아 이동하게 된것이죠.

 

정리하자면 이렇습니다.

기존 자리 이동
0 0
5 1
2 2
4 3
1 4
3 5

이처럼 오른쪽으로 이동할때 5번째 자리에 있던 숫자는 1번째 자리로 이동하며, 4번째 자리에 있던 숫자는 3번째 자리로 이동합니다.

 

그렇게 위로 굴렸을때, 아래로 굴렸을때, 왼쪽으로 굴렸을때, 오른쪽으로 굴렸을때는 위의 그림의 전개도와 같습니다.

자, 이것을 이제 배열로 표현해보지요.

 

int dice[6];
int perm[5][6] = {
	{},
	{ 0,5,2,4,1,3 },{ 0,4,2,5,3,1 },
	{ 3,0,1,2,4,5 },{ 1,2,3,0,4,5 }
};


주어지는 입력에서 주사위를 굴릴때 동쪽은 1(오른쪽으로 굴릴 때), 서쪽은 2(왼쪽으로 굴릴 때), 북쪽은 3(위쪽으로 굴릴때), 남쪽은 4(아래쪽으로 굴릴 때) 이니까 그 순서대로 배열에 초기화했습니다. 0번은 동서남북으로 쓰여지 않으니 비어두었습니다.

그리고 dice라는 배열은 실제 주사위를 의미합니다. 여기에 주사위에 실제 적혀있는 숫자가 저장이 되죠.

 

이제 perm이라는 2차원 배열과 dice가 어떻게 동작하게 될까요? 우선 주사위 dice에는 다음과 같이 {5, 4, 3, 6, 8, 9}라는 수가 적혀져있다고 가정해보겠습니다. 이것을 오른쪽으로 굴리면 dice의 숫자는 어떻게 변할까요?

dice는 perm의 값의 따라 위치를 찾아 이동합니다. 오른쪽으로 이동할때 0번의 숫자는 0번으로, 5번의 숫자는 1번으로, 2번의 수는 2번으로, 4번의 수는 3번으로, 1번의 수는 4번으로, 3번의 수는 5번으로 이동하게 됩니다.

코드로 구현하면 이렇습니다.

 

 

 

 

 

 

void roll(int d) {
	int temp[6];
	for (int i = 0; i < 6; i++)
		temp[i] = dice[perm[d][i]];
	for (int i = 0; i < 6; i++)
		dice[i] = temp[i];
}

 

roll이라는 함수는 방향 d를 인자로 받습니다. 이렇게 d(1은 오른쪽, 2는 왼쪽, 3은 위쪽, 4는 아래쪽)에 따라서 perm을 결정하고 어떻게 dice를 배열할지를 정하게 됩니다. 

 

이를 토대로 이제 전체 문제를 풀면 됩니다.

int map[21][21], command[1001], n, m, k;
int perm[5][6] = {
	{},
	{ 0,5,2,4,1,3 },{ 0,4,2,5,3,1 },
	{ 3,0,1,2,4,5 },{ 1,2,3,0,4,5 }
};

int inc[5][2]{ {},{ 0,1 },{ 0,-1 },{ -1,0 },{ 1,0 } };
int dice[6];

void roll(int d) {
	int temp[6];
	for (int i = 0; i < 6; i++)
		temp[i] = dice[perm[d][i]];
	for (int i = 0; i < 6; i++)
		dice[i] = temp[i];
}


void solve(int next, int y, int x) {
	if (next == k) return;
	int dir = command[next];
	int Y = y + inc[dir][0], X = x + inc[dir][1];
	if (Y < 0 || Y >= n || X < 0 || X >= m) {
		solve(next + 1, y, x);
		return;
	}

	roll(dir);
	if (map[Y][X] == 0) map[Y][X] = dice[1];
	else {
		dice[1] = map[Y][X];
		map[Y][X] = 0;
	}
	printf("%d\n", dice[3]);
	solve(next + 1, Y, X);
}

 

solve라는 함수가 이제 문제를 푸는 재귀함수입니다. 인자 next는 다음 방향으로 이동하라는 인덱스입니다. 이것을 comand가 사용합니다.

command라는 배열은 어떻게 이동할지를 담고 있는 변수입니다. 동서남북 이동이 그것이죠. 동서남북 이동 입력이 1000개가 주어지니 command의 배열을 1000+1개로 잡아놓았습니다.

inc는 어떻게 주사위 위치를 증가할지를 담고 있는 변수입니다. inc는 방향에 따라서 (1,2,3,4) 1이이면 x축으로 1증가, 2이면 x축으로 1감소,.. 이런 식입니다.

 

solve함수에서 만일 범위 밖이면 다음 명령어를 실행하죠. 그것이 조건문에 걸려있군요. 조건에 걸리지 않았다면 주사위를 방향에 따라 굴리고, 이동했던 판의 숫자가 0이면 주사위의 아랫면의 숫자를 판에 복사하며 그것이 아니라면 판의 숫자를 주사위의 아랫면에 복사하고 그 판의 숫자를 0으로 바꾸어줍니다. 그 후 이제 윗면의 수 dice[3]을 출력하면 되지요.

 

전체 코드는 아래와 같습니다.

 

 

 

 

#include <cstdio>

using namespace std;
int map[21][21], command[1001], n, m, k;
int perm[5][6] = {
	{},
	{ 0,5,2,4,1,3 },{ 0,4,2,5,3,1 },
	{ 3,0,1,2,4,5 },{ 1,2,3,0,4,5 }
};

int inc[5][2]{ {},{ 0,1 },{ 0,-1 },{ -1,0 },{ 1,0 } };
int dice[6];

void roll(int d) {
	int temp[6];
	for (int i = 0; i < 6; i++)
		temp[i] = dice[perm[d][i]];
	for (int i = 0; i < 6; i++)
		dice[i] = temp[i];
}


void solve(int next, int y, int x) {
	if (next == k) return;
	int dir = command[next];
	int Y = y + inc[dir][0], X = x + inc[dir][1];
	if (Y < 0 || Y >= n || X < 0 || X >= m) {
		solve(next + 1, y, x);
		return;
	}

	roll(dir);
	if (map[Y][X] == 0) map[Y][X] = dice[1];
	else {
		dice[1] = map[Y][X];
		map[Y][X] = 0;
	}
	printf("%d\n", dice[3]);
	solve(next + 1, Y, X);
}

int main() {
	int y, x;
	scanf("%d %d %d %d %d", &n, &m, &y, &x, &k);

	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			scanf("%d", &map[i][j]);

	for (int i = 0; i < k; i++) scanf("%d", &command[i]);
	solve(0, y, x);
}

 

반응형
블로그 이미지

REAKWON

와나진짜

,

2667 단지번호붙이기


문제 해석


가로 세로 크기가 5에서 25사이의 정사작형 배열에서 1이 뭉쳐져있는 구역을 구하고, 각 구역에서 1이 몇개나 있는지 오름차순으로 출력하라는 문제입니다.

1이 상하좌우에 있다면 그것은 같은 그룹에 속합니다. 아래 사진을 보세요.

<그림 1>에서 1의 그룹은 새 개의 구역으로 <그림 2>로 표현될 수 있습니다. 1번 그룹, 2번 그룹, 3번 그룹이 있고, 각각의 그룹은 7, 8, 9개의 1이 뭉쳐져있는 것이죠. 



입력은 가로, 세로의 길이가 주어지고, 위의 그림처럼 띄어지지 않은 문자열형태로 넘어옵니다. 이렇게 말이죠.


7

0110100

0110101

1110101

0000111

0100000

0111110

0111000




출력은 그룹의 갯수와 각 그룹이 포함하고 있는 1의 개수를 오름차순으로 출력합니다. 


3 (그룹 개수)

7 (각 그룹이 포함하고 있는 1을 오름차순으로 출력)

8

9



풀이


DFS(Depth First Search)를 이용하면 쉽게 풀 수 있습니다. 우선 입력이 불친절하게 들어오니까 이를 정수형 배열로 바꾸어줍니다. 제가 문자 배열은 왠만하면 사용하기 싫거든요.

scanf("%d", &n);
for (int i = 0; i < n; i++) {
	char line[MAX_N];
	scanf("%s", line);
	for (int j = 0; j < n; j++) 
		map[i][j] = line[j] - '0';
}

배열의 길이 n을 입력받은 후에 for루프를 n번 돌면서 문자 배열 line으로 배열의 한줄을 입력받습니다. 그 for문의 내부 for문에서는 map배열에 실제 입력을 시킵니다. 그래서 map에는 입력들을 정수형 배열로 입력됩니다.

int cnt = 0;
for (int i = 0; i < n; i++) 
	for (int j = 0; j < n; j++) 
		if (map[i][j] == 1) {
			groups[cnt]=dfs(i, j);
			cnt = cnt + 1;
		}

입력을 받았으면 map을 전부 이중for문으로 돌며 1이 있는 부분에서 멈춥니다. 1이 있는 부분에서 멈추게 되면 그 지점에서 dfs를 호출하는 것이죠. dfs가 몇번 호출이 되느냐에 따라서 cnt가 커지게 됩니다. 바로 cnt가 그룹의 전체 갯수를 나타냅니다.

groups에는 cnt라는 그룹에 몇개의 1이 포함이 되어있는지 저장됩니다.




dfs에서 무슨 동작을 할까요? 이 문제를 풀기위한 핵심인 dfs를 보도록합시다.

int dfs(int y, int x) {
	if (y < 0 || x < 0 || y >= n || x >= n || map[y][x] == 0) return 0;
	map[y][x] = 0;
	return 1 + dfs(y + 1, x) + dfs(y - 1, x)
		+dfs(y, x + 1) + dfs(y, x - 1);
}

dfs함수는 바로 위와 같이 정의되어 있습니다. y는 행, x는 열만을 인자로 받아들이고 있습니다. y나 x가 배열 범위 밖에 있다면 더이상 dfs를 호출하지 않습니다. 또는 map[y][x]가 0인 부분도 기저사례에 포함이 됩니다. 지금까지 말했던 것들이 첫번째 if문의 조건이 되겠네요.


배열 범위에 포함이 되어있으며 map[y][x]=1이라면 바로 그 지점을 0으로 바꿔줍니다. 왜 바꿔주냐면 더 이상 이 점을 방문할 필요가 없으니까요. 만약 map[y][x]=0이 없다면 이 프로그램은 영원히 끝나지 않을겁니다.

그 후에는 상(y-1)하(y+1)좌(x-1)우(x+1)에 dfs를 호출합니다. return에 1을 더해야만 최종 dfs에는 그룹의 1의 개수가 return됩니다.


printf("%d\n", cnt);
sort(groups, groups + cnt);
for (int i = 0; i < cnt; i++) 
	printf("%d\n",groups[i]);

이제 답은 전부 구했네요. cnt를 출력하고 오름차순으로 1의 개수를 출력하라 하였으니 정렬 후에 groups를 출력하면 됩니다.




아래는 전체코드입니다.

#include <cstdio>
#include <queue>
#include <algorithm>
using namespace std;

#define MAX_N 26

int n;
int map[MAX_N][MAX_N];
int groups[MAX_N*MAX_N];
int dfs(int y, int x) {
	if (y < 0 || x < 0 || y >= n || x >= n || map[y][x] == 0) return 0;
	map[y][x] = 0;
	return 1 + dfs(y + 1, x) + dfs(y - 1, x)
		+dfs(y, x + 1) + dfs(y, x - 1);
}
int main() {
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		char line[MAX_N];
		scanf("%s", line);
		for (int j = 0; j < n; j++) 
			map[i][j] = line[j] - '0';
	}

	int cnt = 0;
	for (int i = 0; i < n; i++) 
		for (int j = 0; j < n; j++) 
			if (map[i][j] == 1) {
				groups[cnt]=dfs(i, j);
				cnt = cnt + 1;
			}
		
	
	printf("%d\n", cnt);
	sort(groups, groups + cnt);
	for (int i = 0; i < cnt; i++) 
		printf("%d\n",groups[i]);
	
	return 0;
}


dfs만 알면 그리 어렵지 않게 풀수 있는 문제였습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

1697 숨바꼭질



문제 해석


수빈이가 동생을 찾아가는데 몇초만에 찾는지 알아내는 문제입니다. 수빈이와 동생은 점 0~100000사이에 있습니다. 근데 왜 수빈이만 동생을 찾는 건지는 이해가 가지 않는군요. 

수빈이는 다음과 같은 이상한 능력이 있습니다.


1. 수빈이가 위치 X에 있을때 1초에 X+1, X-1의 위치로 걸을 수 있습니다. 물론 범위 0~100000의 범위 안에서만 말이죠.

2. 수빈이는 순간이동을 할 수 있는데, 현재 위치 X에서 1초만에 X*2의 위치로 이동할 수 있습니다.


두개의 입력이 주어지는데, 가장 첫번째 입력은 수빈이의 위치, 두번째 입력은 동생의 위치입니다.

5와 17이라는 입력이 주어진다면 답은 4가 됩니다. 5->10->9->18->17로 찾을 수 있습니다.


이런 조건 속에서 몇 초만에 동생을 찾을 수 있을까요?




풀이


BFS(Breadth First Search)를 이용하면 이 문제를 풀 수 있습니다. 그 때문에 큐(Queue)를 이용해야합니다. 수빈이가 있는 점을 start라고 하고 동생이 있는 위치를 end라고 합시다. 결국 우리가 구현해야하는 것은 start에서부터 BFS를 통해 end까지 도달을 몇번에 할 수 있는가입니다.

아래의 전체적인 코드를 우선 보고 설명하도록 하겠습니다.


#include <cstdio>
#include <queue>

using namespace std;
#define MAX_N 100001
#define LIMIT 100000
int bfs(int start, int end) {
	if (start==end) return 0;
	queue<int> q;
	q.push(start);
	int length[MAX_N] = { 0, };

	while (!q.empty()) {
		int here = q.front();
		q.pop();
		if (here + 1 <= LIMIT) {
			if (here + 1 == end)
				return length[here] + 1;
			if (!length[here + 1]) {
				length[here + 1] = length[here] + 1;
				q.push(here + 1);
			}
		}
		if (here - 1 >= 0) {
			if (here - 1 == end) return length[here] + 1;
			if (!length[here - 1]) {
				length[here - 1] = length[here] + 1;
				q.push(here - 1);
			}
		}
		if (here * 2 <= LIMIT) {
			if (here * 2 == end) return length[here] + 1;
			if (!length[here * 2]) {
				length[here * 2] = length[here] + 1;
				q.push(here * 2);
			}
		}

	}
	return -1;
}
int main() {
	int start, end;
	scanf("%d %d", &start, &end);
	printf("%d\n", bfs(start, end));
	return 0;
}

코드에서 가장 눈여겨 봐야할 곳이 bfs함수입니다. bfs는 두개의 인자를 전달받는데 우리가 아까 이야기했던 start와 end이지요.
bfs내부에서는 start와 end가 같은 위치일 수 있으므로 가장 먼저 start와 end가 같은지 확인하며 참이라면 bfs를 순회할 필요없이 바로 0을 반환해줍니다.




이제 본격적인 bfs 탐색을 시작합니다. q에는 다음 순회할 노드들이 들어가며, length배열에는 그 점까지 몇번에 걸쳐 오는가를 저장하게 됩니다. 예를 들어 5->10->11->12까지 순회했다고 친다면 length[5]=0, length[10]=1, length[11]=2, length[12]=3이라는 값을 갖게 되는 겁니다. 이 변수에 0이 아닌 양수의 값이 있다는 것은 이미 그 점을 방문한 것이 되므로 q에 이 점을 push하지 않습니다.

처음 start의 점을 큐에 push한 이후에 q가 빌때까지 while루프를 돕니다. while루프 안에는 here라는 변수로 큐의 점을 꺼내옵니다. 처음에는 start가 들어있으니, start의 점을 꺼내오겠죠?
그 점이 here가 되고, here에서 +1, -1, *2가 범위에 속해있으며, 그 점이 바로 end와 같은 점이라면 현재 length[here]에서 1을 추가한 값을 반환합니다.
end와 같은 점이 아닐 경우에는 length에 다음 q에 추가될 점(here+1, here-1, here*2)이 기록이 되었나(length[here+1], length[here-1], length[length*2]에 0이 아닌 양수의 값이 있으면 이미 그 점을 전에 방문했다는 겁니다.)를 확인하고 length에 기록이 되어있지 않다면 length를 기록하고 q에 push합니다.

bfs함수에서 while루프의 if조건문에 의한 반환이 반드시 이루어집니다. +1,-1을 통해서 0~100000까지 전부 방문 가능하니까요.

결론은 bfs를 이용하면 그리 어렵지 않게 풀수 있는 문제였습니다.


반응형
블로그 이미지

REAKWON

와나진짜

,

IP 클래스와 서브넷마스크


우리가 PC를 사용해 인터넷을 즐기고 게임을 하는 등 네트워크 통신에서는 항상 주소를 갖고 동작하고 있습니다. 그게 바로 IP 주소이지요. IP주소를 통해서 통신을 할 수 있는 겁니다.


우선 나의 아이피는 어떻게 알 수 있을까요?

윈도우에 cmd를 키고 ipconfig를 입력해보세요. 리눅스라면 ifconfig 명령어를 실행시켜보세요. 자신의 아이피주소가 나올겁니다.


이전에 컴퓨터의 수가 별로 없어서 IP주소를 막 뿌려댔으나 폭발적인 PC사용의 증가로 사태의 심각성을 깨달았는지 머리를 쓰게 된 것입니다.

그래서 나온게 서브넷마스크입니다.




본격적으로 IP클래스 대역과 서브넷마스크를 보기에 앞서 IP주소에 대해서 간단히 설명하고 넘어가도록 하겠습니다.


우리는 다음과 같은 IP주소를 갖고 있다고 칩시다.


192.168.10.10 -> 1100 0000. 1010 1000. 0000 1010. 0000 1010


십진수와 이진수로 나타냈습니다. 총 32비트, 4바이트라는 것을 알 수 있습니다. 여기서 1바이트가 바로 옥텟이라고 합니다. Oct가 숫자 8을 나타내는 접두사이거든요(온타곤, 옥토퍼스 등). 이것은 우리가 흔히 보는 IPv4의 주소 표기방식입니다. 


조금 후에 이야기를 하겠지만 이 IP주소가 C클래스 대역에 속한다면 네트워크 주소는 1100 0000. 1010 1000. 0000 0010입니다. 나머지 빨간 부분인 0000 1010은 호스트 주소랍니다. 그래서 192.168.10으로 시작하는 PC는 192.168.10.10과 같은 네트워크에 속하고 있다는 것을 의미합니다.


가장 첫번째 호스트 주소는 네트워크 자체를 지칭하며, 마지막 주소는 브로드캐스트용 주소로 쓰입니다. 예를 들어 위에서 192.168.10.0은 192.168.10의 네트워크를 가리키고, 192.168.10.255가 브로드캐스트용 주소이지요.


우리는 이제 IPv4를 토대로 IP 클래스 대역과 서브넷 마스크에 대해 알아보도록 하겠습니다.


A클래스

A클래스의 첫번째 옥텟의 비트는 0으로 고정됩니다.


0xxx xxxx. xxxx xxxx. xxxx xxxx. xxxx xxxx

그렇기 때문에 표현할 수 있는 범위는 0000 0000.0000 0000.0000 0000.0000 0000~0111 1110.1111 1111.1111 1111.1111 1111입니다.

그래서 0.0.0.0 ~ 127.255.255.255입니다.

이 IP클래스는 대규모 네트워크에 적합합니다.

네트워크 주소는 처음 8비트까지입니다. 나머지 24비트는 호스트 주소를 의미합니다.


B클래스

B클래스는 첫번째 옥텟의 두번째 비트가 고정됩니다. 10으로 고정이 됩니다.

10xx xxxx. xxxx xxxx. xxxx xxxx. xxxx xxxx

그래서 표현할 수 있는 범위는 1000 0000. 0000 0000. 0000 0000. 0000 0000~ 1011 1111. 1111 1111. 1111 1111까지입니다.

그래서 128.0.0.0 ~ 191.255.255.255입니다.

네트워크 주소는 처음 16비트이며 호스트 주소는 나머지 16비트입니다.




C클래스

C클래스는 첫번째 옥텟의 세번째 비트가 110으로 고정됩니다.

110x xxxx. xxxx xxxx. xxxx xxxx. xxxx xxxx

그래서 표현할 수 있는 범위는 1100 0000. 0000 0000. 0000 0000. 0000 0000 ~ 1101 1111. 1111 1111. 1111 1111. 1111 1111까지입니다.

십진수로 표현하면 192.0.0.0 ~ 223.255.255.255 입니다.

네트워크 주소는 처음 24비트이며 나머지 8비트는 호스트 비트입니다.


D클래스

D클래스는 첫번째 옥텟의 네번째 비트가 1110으로 고정됩니다.

1110 xxxx. xxxx xxxx. xxxx xxxx. xxxx xxxx

그래서 표현할 수 있는 범위는 1110 0000. 0000 0000. 0000 0000. 0000 0000 ~ 1110 1111. 1111 1111. 1111 1111. 1111 1111까지입니다.

십진수로 표현하면 224.0.0.0 ~ 239.255.255.255 입니다. 멀티캐스트용 대역으로 IP주소에 할당되지 않습니다.


E클래스

E클래스는 첫번째 옥텟의 네번째 비트가 1111으로 고정됩니다.

1111 xxxx. xxxx xxxx. xxxx xxxx. xxxx xxxx

그래서 표현할 수 있는 범위는 1111 0000. 0000 0000. 0000 0000. 0000 0000 ~ 1111 1111. 1111 1111. 1111 1111. 1111 1111까지입니다.

십진수로 표현하면 240.0.0.0 ~ 255.255.255.255 입니다. 예약된 주소 대역으로 IP주소에 할당되지 않습니다.


D와 E클래스는 특정 용도로 사용하기 때문에 실제 IP주소에 할당되지 않습니다.


클래스 등급이 낮아지면서 호스트 주소 부분이 점점 줄어들고 있는것을 알 수 있습니다.


이 와중에 특정 주소 대역은 사설 IP로 사용합니다. 아래에 나와있습니다.


A클래스 사설 IP주소 10.0.0.0 ~ 10.255.255.255

B클래스 사설 IP주소 172.16.0.0 ~ 172.31.255.255

C클래스 사설 IP주소 192.168.0.0 ~ 192.168.255.255 


또한 0.0.0.0, 255.255.255.255처럼 네트워크 시작 주소, 브로드캐스트용주소는 IP주소로 할당되지 않으면 127.0.0.1과 같은 loopback용 주소 또한 사용할 수 없습니다.


서브넷마스크


우리가 C클래스 대역을 사용해서 호스트를 255개를 수용할 수 있는 것도 너무 많이 남을때가 있습니다. 또는 B클래스 대역을 C클래스 대역으로 쓰고 싶을 때가 있습니다. 네트워크 주소를 조금 더 효율적으로 할당하고자 나온 것이 서브넷마스크입니다. 서브넷마스크로 만들어진 네트워크를 서브넷이라고 합니다.


이제 서브넷마스크를 어떻게 계산하며 네트워크 부분과 호스트부분이 어떻게 되는지 살펴봅시다.




예를 가지고 설명을 하는 것이 가장 좋겠네요.

128.255.11.11는 B클래스 주소입니다. 128.255까지가 네트워크 주소이며 나머지 2옥텟이 나머지 호스트 주소입니다.

128.255.11.11을 255.255.255.0이라는 서브넷 마스크를 씌우면 어떻게 될까요? 서브넷 마스크는 비트로 보는 것이 편합니다.


1000 0000.1111 1111.0000 1011.0000 1011 <- 128.255.11.11

1111 1111.1111 1111.1111 1111.0000 0000 <- 255.255.255.0


여기서 서브넷마스크 비트가 1인것은 전부 네트워크 주소가 됩니다. 반대로 0은 호스트 주소 범위가 되지요. 그래서 128.255.11이 네트워크 주소 대역이 되고 나머지 11이 호스트용 주소가 되겠네요.


서브넷마스크의 표기방식은 주소/서브넷마스크 주소 또는 주소/비트수로 표현할 수 있습니다. 128.255.11.11/255.255.255.0 또는 128.255.11.11/24로 표현가능하다는 것입니다.


조금 더 잘게 쪼개보겠습니다.

128.255.11.11을 255.255.255.224의 서브넷마스크를 적용하면 어떻게 될까요? 이 역시 비트로 풀어보도록 합시다.


1000 0000.1111 1111.0000 1011.0000 1011 <- 128.255.11.11

1111 1111.1111 1111.1111 1111.1110 0000 <- 255.255.255.224


하위 5비트 0 0000을 호스트용 주소로 적용시킬 수 있으니까 128.255.11.0 ~ 128.255.11.31까지가 같은 네트워크이고, 128.255.11.11이 바로 이 네트워크에 속하는 것을 알 수 있습니다.

전부 구해보면 다음과 같습니다.


128.255.11.0 ~ 128.255.11.31 

네트워크 주소 128.255.11.0, 브로드캐스트 주소 128.255.11.31

128.255.11.32 ~ 128.255.11.63

네트워크 주소 128.255.11.32, 브로드캐스트 주소 128.255.11.63

128.255.11.64 ~ 128.255.11.95

네트워크 주소 128.255.11.64, 브로드캐스트 주소 128.255.11.95

128.255.11.96 ~ 128.255.11.127

네트워크 주소 128.255.11.96, 브로드캐스트 주소 128.255.11.127

128.255.11.128 ~ 128.255.11.159

네트워크 주소 128.255.11.128, 브로드캐스트 주소 128.255.11.159

128.255.11.160 ~ 128.255.11.191

네트워크 주소 128.255.11.160, 브로드캐스트 주소 128.255.11.191

128.255.11.192 ~ 128.255.11.223

네트워크 주소 128.255.11.192, 브로드캐스트 주소 128.255.11.223

128.255.11.224 ~ 128.255.11.255

네트워크 주소 128.255.11.224, 브로드캐스트 주소 128.255.11.255




총 8개의 네트워크로 나누어졌음을 알 수 있습니다. (B클래스 대역이긴 하지만 C클래스처럼 0~255까지만 보았습니다.)


사실 위의 A,B,C클래스 대역은 서브넷마스크가 255.0.0.0, 255.255.0.0, 255.255.255.0의 서브넷마스크가 적용되었다는 사실을 눈치채셨나요? 이것을 default subnet mask라고 합니다.


반응형
블로그 이미지

REAKWON

와나진짜

,

TCP/IP

인터넷 프로그램들이 서로 통신을 하는데 있어서 여러 프로토콜이 있습니다. 인터넷 프로토콜에서 가장 많이 사용하는 대표적인 프로토콜은 여러분들도 많이 아시다시피 IP입니다. 여기서 중요한 것은 TCP/IP는 계층이 아니라 프로토콜이라는 사실이라는 사실을 주의해주세요.


TCP/IP는 OSI7 계층과는 조금은 다른 TCP/IP의 구조적인 계층 위에서 동작합니다.


지난 번에 OSI7 계층에 대해서 알아보았는데요. TCP/IP 계층은 OSI7계층과 비교하여 어떤 점이 다른지 살펴보는 시간을 가져보도록 하지요.






OSI7 계층과는 조금은 다른 모습을 볼 수 있습니다. 보세요.

우선 계층의 수 부터가 다릅니다. OSI는 7계층인데 반해 TCP/IP 계층은 4계층이 전부라는 것을 알 수 있습니다.


이제 조금 더 세세하게 살펴보도록 하지요.




1. 네트워크 인터페이스 계층 (Network Interface Layer)

이 계층은 Node-To-Node간의 신뢰성 있는 데이터 전송을 담당하는 계층입니다. OSI7 계층의 물리 계층과 데이터링크 계층의 역할을 바로 이 계층이 담당하는 것으로 볼 수 있네요.


따라서 MAC주소가 이 계층에서 사용됩니다. MAC주소는 OSI7 계층에서 데이터링크 계층의 주소였죠?? 


네트워크 인터페이스 계층이 바로 데이터링크 계층까지 담당하니까 MAC 어드레스가 사용되는 겁니다.


혹시 랜카드라고 들어보셨나요? 바로 이거말이에요.




정확한 명칭은 NIC라고 하여 Network Interface Card입니다. 바로 이 랜카드가 있어야만 네트워크 통신을 할 수 있는데, 이름에서도 알 수 있듯이 네트워크 인터페이스 계층에서 동작하는 장비입니다.


주요 프로토콜을 무엇이 있을까요?

LAN상에서는 Ethernet, TokenRing, FDDI 등이 있으며 WAN 상에서는 X.25, Frame Relay, PPP 등이 있습니다.


2. 인터넷 계층 (Internet Layer)

OSI7계층의 네트워크 계층을 담당하는 계층입니다. OSI7 계층처럼 호스트간의 라우팅을 담당하지요. 


인터넷 계층에서 동작하는 프로토콜에는 무엇이 있을까요? 대표적인 몇가지 프로토콜을 살짝 알아보도록 합시다.


IP(Internet Protocol) : 비신뢰성, 비연결지향 데이터그램 프로토콜입니다. 

ARP(Address Resolution Protocol) : 주소변환 프로토콜입니다. IP주소를 MAC주소로 변환하는 프로토콜이지요.

RARP(Reverse ARP) : 반대로 MAC주소로 IP주소를 찾는 프로토콜입니다.

ICMP(Internet Control Message Protocol) : 상태 진단 메시지 프로토콜인데요. 이 프로토콜을 이용하는 대표적인 프로그램이 ping입니다.

IGMP(Internet Group Message Protocol) : 멀티캐스트용 프로토콜입니다.




3. 전송 계층 (Transport Layer)

OSI7 계층의 전송계층과 같습니다. 프로세스간의 신뢰성 있는 데이터 전송을 담당하는 계층입니다.


process-to-process 전송을 담당하기 위해서는 논리적 주소가 필요한데요. process가 사용하는 포트 번호를 그 논리적 주소로 사용합니다.


전송 계층에서 프로토콜은 무엇이 있을까요?


TCP (Transmission Control Protocol) : 신뢰성있는 연결지향형 프로토콜입니다. 신뢰성있다는 말은 그 페킷에 대한 오류처리나 재전송따위로 에러를 복구하는 것을 말합니다. 그때문에 TCP의 헤더에 붙는 정보가 많습니다.

UDP (User Datagram Protocol) : 비신뢰성 비연결형 프로토콜입니다. 페킷을 잃거나 오류가 있어도 대처하지 않는 것을 말합니다. 따라서 UDP헤더는 간단한 구조를 갖고 있습니다.



4. 응용 계층 (Application Layer)

사용자와 가장 가까운 계층입니다. OSI7계층의 5계층부터 7계층까지의 기능을 담당하고 있지요.

서버나 클라이언트 응용 프로그램이 이 계층에서 동작합니다. 우리가 알고 있는 브라우저나 텔넷같은 서비스가 이 계층에 동작하며, 동작하기 위해서는 전송계층의 주소, 즉 포트번호를 사용합니다.


이를테면 http는 포트번호 80번을 사용하지요.


역시 프로토콜은 무엇이 있나 살펴볼까요?


HTTP (Hyper-Text Transfer Protocol) : TCP기반의 프로토콜로 포트번호 80번을 사용합니다.

Telnet : TCP 포트번호 23번을 사용합니다. 원격 터미널을 접속할때 이 포로토콜을 사용합니다.

SSH (Secure Shell) : 텔넷과 같은 서비스는 보안에 취약합니다. 비밀번호가 암호화되지 않아 그대로 노출이 되기 때문이지요. 이것을 보완한것이 SSH입니다. 포트번호 22번을 사용합니다.

FTP(File Transfer Protocol) : 파일 전송 프로토콜입니다. 파일을 받거나 올릴때 FTP를 사용하지요. FTP는 파일을 올리거나 내려받을때 신뢰성을 중요시하기 때문에 TCP에서 동작하구요. 2개의 포트를 사용합니다. 

TCP 포트 20번은 데이터 전송을 위한 용도, TCP 포트 21번은 제어용으로 사용합니다.

SMTP (Simple Mail Transfer Protocol) : 메일 전송 프로토콜입니다. TCP 상에서 동작하며 포트는 25번을 사용합니다.

POP3 (Post Office Protocol Version3) : 메일 수신용 프로토콜입니다. 아웃룩같은 프로그램이 POP3라는 프로토콜을 사용하여 동작합니다. TCP 포트 110번을 사용합니다.

DNS (Domain Name System) : 도메인명에 대한 호스트 정보를 제공해줍니다. 기본적으로 UDP상에서 동작합니다. 기본적으로 실패하면 다시 한번 요청하면 되며 그렇게 중요한 정보가 아니기 때문이죠. 하지만 신뢰성을 요할 경우에는 TCP상에서도 동작합니다. 데이터의 길이가 길 경우같은 때 TCP 기반으로 동작할 수 있습니다.

UDP, TCP 포트 53번을 사용합니다.




이와 같이 포트번호가 특정 프로토콜이 사용해서 우리가 쓸 수 없는 포트들이 있습니다. 이런 포트들을 well-known port라고 합니다.


프로토콜 헤더 정보를 잘 읽고 분석할 수 있다면 네트워크를 더 잘 이해할 수 있을 겁니다.


따라서 다음 시간에는 헤더를 보고 무슨 정보가 있는지 살펴보는 기회를 갖도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

LCS(Longest Common Subsequence)

백준 9251번입니다. 문제는 이렇습니다.


임의의 두 수열에서 공통적으로 갖고 있는 가장 긴 부분 수열의 길이를 구하는 것이 문제입니다.

예를 들어 다음과 같은 두 수열이 있다고 합시다.


ACAYKP

CAPCAK


첫번째 수열과 두번째 수열의 공통수열은 다음과 같습니다.


ACAYKP

CAPCAK


▶ AAK


ACAYKP

CAPCAK


▶ CAK

ACAYKP

CAPCAK


▶ ACAK


이 외에도 여러가지 공통 수열이 있지만 가장 긴 수열은 4의 길이를 갖고 있는 ACAK입니다. 두 수열에서 LCS는 여러가지가 있지만 그 길이는 같지요.


제약조건

두 수열을 항상 대문자 알파벳으로 주어지고 각 수열의 길이는 1이상 10000이하입니다.



풀이


이 문제는 생각보다 단순합니다. 두 수열의 문자를 비교해 일치하면 동시에 한칸을 옮겨서 그 다음부터 비교하면 되고, 만약 일치하지 않는다면 두 수열중 아무것이나 한칸 옮겨서 그 다음부터 비교하면 됩니다.


A O L D M K E P P A

O C A C K E M P P A 


위 두 수열의 LCS길이는 AKEPPA 또는 OKEPPA입니다. 그렇기때문에 답은 6이 나오지요. 이 답이 나오는 과정을 살펴보도록 하겠습니다.


O L D M K E P P A

C A C K E M P P A


빨간색 글자는 현재 비교할 두 문자입니다. 우선 초기상태이므로 A와 O가 비교됩니다. 두 문자가 일치하지 않으므로 A의 다음 문자를 비교하거나 O의 다음 문자를 비교합니다. 저는 일부러 답을 찾기 위해서 최적의 방법을 사용할 것이니 여러분들은 두 수열 중 어떤 수열을 먼저 한칸 더 옮기든지 상관없다는 것만 아시면 됩니다.





A O L D M K E P P A

O C A C K E M P P A

첫번째 수열의 비교 문자를 한칸 옮기니까 두 수열의 문자가 일치하는 군요. 그렇다면 두 수열의 비교문자를 다음으로 옮겨줍니다.


A O L D M K E P P A

O C A C K E M P P A


L과 C는 일치하지 않으므로 두 수열중 비교할 문자를 한칸 뒤로 옮깁니다.


A O L D M K E P P A

O C A C K E M P P A


이런식으로 이제 계속 비교하다가 보면 언젠가는 다음과 같은 상황이 발생합니다.


A O L D M K E P P A

O C A C K E M P P A


이제 K와 K를 비교할 때가 된거죠. K는 두 수열이 두 수열의 비교문자를 모두 다음 위치로 이동합니다.


A O L D M K E P P A

O C A C K E M P P A


E 역시 두 문자가 같으므로 다시 또 한칸 동시에 비교문자를 옮깁니다. 옮기고 난 후 보니까 P와 M은 일치하지를 않지요. 그러니까 둘 중 아무 수열이나 비교할 문자를 한칸 옮겨줍니다. 저는 답을 찾기위해 일부터 아래 수열의 비교문자를 한칸 더 옮기겠습니다.


A O L D M K E P P A

O C A C K E M P P A


이제 P는 두 수열 모두 같으니 다음 칸으로 옮겨줍니다. 계속 이런식으로 답을 찾으면 결국 다음과 같은 LCS가 나오게 되는 것이죠.


A O L D M K E P P A

O C A C K E M P P A


이 LCS는 OKEPPA이고 길이가 6인 것을 알 수 있습니다. 따라서 답은 6이 되는 겁니다.


DP적용

이렇게 답은 찾았는데요. 길이가 10000인 두 수열을 이런식으로 계속 비교하다가는 시간안에 답을 찾기가 힘듭니다. 우리가 자세히 살펴본다면 DP를 적용할 수 있다는 사실을 알 수 있는데요. 만일 수열에서 순서가 A와 O를 비교했는데 아래 배열의 비교 문자를 옮긴다면 A와 C를 비교하게되어 O를 빠뜨리고 가지요. 분명 O는 일치하는 문자임에도요. 


[1][2][3][4][5][6][7][8][9][10]

A O L D M K E P P A


[1][2][3][4][5][6][7][8][9][10]

O C A C K E M P P A


그래서 결국 KEPPA까지만 일치하게 된 상황에서 각 인덱스는 [6][5]입니다. 

따라서 [6][5]에서 가장 긴 수열은 5라고 기억하지요. 그리고 다시 이전으로 돌아가서 비교해야합니다. 그때는 순서를 바꿔 O가 일치하는 상황이 되죠.

   

[1][2][3][4][5][6][7][8][9][10]

A O L D M K E P P A


[1][2][3][4][5][6][7][8][9][10]

O C A C K E M P P A


이렇게 [6][5]까지 비교한다면 이미 기억해두었으니 제차 끝까지 비교하지 않고 [6][5]까지의 값 5를 반환하기면 하면 됩니다. 그리고 O는 일치했으므로 1을 더하는것이죠. 그래서 답이 6이 됩니다. 이렇게 DP를 적용해서 시간을 줄일 수 있습니다.





구현

다음의 코드는 LCS의 길이를 구하는 코드입니다. 위의 설명을 잘 이해했다면 코드 역시 이해하기가 훨씬 쉬울거에요.

#include <stdio.h>
#include <string.h>

#define MAX(a,b) a>b ? a:b
#define _CRT_SECURE_NO_WARNINGS
char a[1001], b[1001];
int n, m;
int dp[1001][1001];
int solve(int pos1, int pos2) {
	if (pos1 == n || pos2 == m) return 0;
	if (a[pos1] == b[pos2])
		return 1 + solve(pos1 + 1, pos2 + 1);

	int ret = 0;
	if (dp[pos1][pos2] != -1) return dp[pos1][pos2];
	ret = MAX(solve(pos1 + 1, pos2), solve(pos1, pos2 + 1));
	return dp[pos1][pos2] = ret;
}
int main() {

	scanf("%s %s", a, b);
	n = strlen(a); m = strlen(b);
	memset(dp, -1, sizeof(int) * 1001 * 1001);
	int ret = solve(0, 0);
	printf("%d\n", ret);
}

pos1은 첫번째 수열의 비교할 문자 위치, pos2는 두번째 수열의 비교할 문자위치를 말합니다. 만약 pos1과 pos2이 하나라도 수열 끝까지 갔다면 종료되는 것이죠. 이것이 기저 사례입니다.

앞서 말한것과 같이 첫번째 수열의 pos1위치의 문자와 두번째 수열의 pos2위치 문자와 비교해서 같다면 LCS의 길이 1을 더해주고 비교할 문자의 위치 pos1,pos2를 한 칸 증가시켜줍니다.

만약 일치하지 않는다면 둘 중 하나의 수열 비교문자(pos1 또는 pos2)만 한칸 증가시키고 둘 중 가장 큰 값을 반환하면 되죠.

나중에 return할때 메모이제이션으로 값을 업데이트 시키는 것은 잊지 말고 말이죠.

반응형
블로그 이미지

REAKWON

와나진짜

,

시간 관련 함수

프로그램에서 시간은 거의 필수로 다루어질 만큼 중요한 요소입니다. 그래서 우리는 시간을 잘 다루어야 할 필요가 있습니다.  그래서 이번에는 C언어에서 시간과 관련된 함수 몇가지를 살표보도록 하지요. 지금부터 다룰 함수는 모두 시간과 관련된 함수이므로 time.h 헤더파일을 포함시켜야한다는 것을 잊지 마세요.

time

가장 기본적인 시간 관련 함수입니다. 함수의 원형은 다음과 같죠.

#include <time.h>

time_t time(time_t *timeptr);

 

이 함수는 1970년 1월 1일 0시 (UTC)부터 인자값(timeptr)까지 흐른 시간을 반환합니다. 그 단위가 초단위지요.  이 함수의 사용방법은 주로 2가지 입니다.

timeptr이라는 매개변수에 인자를 전달하여 현재까지 흐른 시간을 초단위로 구하거나, time함수에 NULL을 전달하여 반환값을 받거나 입니다.

 

 

 

그래서 현재시간까지 구한다면 time(NULL)을 호출해서 반환값을 받으면 되는 것이죠. 아래의 예처럼요.

time_t t; t = time(NULL); printf("%ld\n", t); 

그 결과는 아래처럼 큰 정수를 반환하게 되지요. 1970년 1월 1일 0시(UTC)부터 지금까지 초단위의 시간을 구하기 때문이지요.

1548588718

 

localtime

이 함수는 지역시간을 구해냅니다. 반환하는 형식은 구조체인데 tm이라는 구조체입니다. 다음 함수의 원형처럼 말이죠.

#include <time.h>

struct tm *localtime(const time_t *timeval);

 

우리는 위의 time함수에서 UTC기준으로 흐른 시간을 초단위로 구했지요. 초단위로 구하니 보기도 어렵고 지금이 몇월인지 몇년도인지 구분하기가 어렵습니다. 그래서 이것을 사람이 잘 알아보게끔 구조체로 변환할 수 있는 함수가 바로 localtime함수입니다.

 

 

 

 

우리는 반환값인 tm구조체를 다는 아니어도 필요한 몇가지 멤버는 알아야할 필요가 있습니다.

struct tm {

        int tm_sec;   //초

        int tm_min;   //분

        int tm_hour;   //시

        int tm_mday;  //일

        int tm_mon;    //월(0부터 시작)

        int tm_year;    //1900년부터 흐른 년

        int tm_wday;  //요일(0부터 일요일)

        int tm_yday;  //현재 년부터 흐른 일수

        int tm_isdst; 

};

필드명은 꽤 직관적입니다. 이 필드명만 보아도 무엇을 의미하는 것인지 잘 알것이라고 생각합니다.

자세한 것은 코드로 살펴보도록 합시다.

#include <stdio.h>
#include <time.h>

int main() { 
        time_t current;
        time(&current); 
        struct tm *t = localtime(&current);

        printf("%d년 %d월 %d일 ", 
                        1900 + t->tm_year, t->tm_mon + 1, t->tm_mday); 

        switch (t->tm_wday) {
                case 0:printf("일요일 "); 
                       break;
                case 1:printf("월요일 ");
                       break;
                case 2:printf("화요일 "); 
                       break; 
                case 3:printf("수요일 "); 
                       break;
                case 4:printf("목요일 "); 
                       break; 
                case 5:printf("금요일 ");
                       break;
                case 6:printf("토요일 "); 
                       break;
        } 

        printf("%d:%d:%d\n", t->tm_hour, t->tm_min, t->tm_sec);
        printf("1년 365일 중 %d일째\n", t->tm_yday + 1); 
}

 

그 결과는 다음과 같습니다.

2019년 1월 27일 일요일 20:55:9

1년 365일 중 27일째

각각 년, 월, 일 , 시, 분, 초, 요일을 각각 구할때, 또는 날짜계산, 시간계산을 할때 편리하겠군요.

 

ctime

 

#include <time.h>

char *ctime(const time_t *time);

구조체를 굳이 사용하고 싶지 않고 사용자가 시간을 읽을 수 있게끔 문자열로 변환하는 것이 ctime함수입니다. 마찬가지로 time함수에서 반환된 값을 ctime에 인자로 쏙 집어넣으면 현재 시간 정보를 다음과 같이 보기좋은 형식으로 반환해줍니다.

Www Mmm dd hh:mm:ss yyyy

 

Www 요일을 영어 세글자로 나타냅니다.

Mmm 월을 영어 세글자로 나타냅니다.

dd 날짜를 숫자 두글자로 나타냅니다.

hh 시를 숫자 두글자로 나타냅니다.

mm 분을 숫자 두글자로 나타냅니다.

ss 초를 숫자 두글자로 나타냅니다.

yyyy 년도를 숫자 네글자로 나타냅니다.

 

 

 

 

ctime이 어떤 함수인지 알았으면 어떻게 사용하는지 코드를 보세요. 몇 줄 안되는 코드입니다.

#include <stdio.h>
#include <time.h>

int main() { 
        time_t t = time(NULL); 
        printf("현재시간 :%s\n", ctime(&t));
}

 

코드는 상당히 간단합니다. 어떻게 나오는지 살펴보지요.

현재시간 :Sun Jan 27 21:07:50 2019

 

asctime

이 함수는 ctime과 같이 시간을 문자열로 출력해주지만 인자로 tm구조체 포인터를 받는 것과 ctime과 다른게 없습니다.

#include <time.h>

char *asctime(const struct tm *tm);

반환하는 문자열 포맷도 ctime과 똑같습니다. 사용방법은 그저 tm구조체 포인터를 전달하기만 하면 됩니다.

아래의 사용예를 보세요.

#include <stdio.h>
#include <time.h> 

int main() {
        time_t current = time(NULL);
        struct tm *t = localtime(&current); 
        printf("현재시간 :%s\n", asctime(t));
}
결과는 위의 ctime 결과와 같습니다.
여기까지 시간관련된 함수 4개를 살펴보았는데, 여기까지만 알면 충분할 것 같군요. 나중에 기회가 되면 더 살펴보도록 하지요.
반응형
블로그 이미지

REAKWON

와나진짜

,

비트연산자


컴퓨터가 사용하는 모든 데이터들은 전부 1과 0으로 이루어진 비트열이라는 것을 다들 잘 알겁니다.


C언어에서도 역시 그렇습니다. 우리는 정수형 변수 a에 16이라는 데이터를 집어넣는 것은 사실 코딩할때만 그렇습니다. 하지만 실행이 될때는 이진수로 메모리에 저장이 되어있죠.


만약

int a=16

이라고 정수를 메모리에 넣어준다면 이렇게 메모리에 잡히게 됩니다.

int는 4바이트의 메모리를 갖고 있으므로 

0000 0000 0000 0000 0000 0000 0001 0000

이렇게 저장이 되지요.


우리는 이러한 비트로 연산을 수행할 수 있습니다. 오늘은 이런 비트 연산에 대해서 알아보도록 하겠습니다.


NOT ( ~ )

NOT연산자는 비트열을 반전시키는 연산자입니다. 직관적으로 이해하기도 쉽습니다.

만약 비트가 1이면 0으로 반전하고, 0이면 1로 바꾸기만 하면 되니까요. 연산자 기호는 ~를 씁니다.


그래서 만약

~1000 0110 는 0111 1001로 바뀌게 됩니다.


 

OR( | )

OR 연산자는 | 입니다. 엔터 위쪽 \가 보이시나요? 이것을 쉬프트기로 눌러 입력한게 바로 OR연산자 | 입니다. A OR B는 A 또는 B가 1이라면 답은 1이 되는 겁니다.


0 | 0 = 0

0 | 1 = 1

1 | 0 = 1

1 | 1 = 1


두 비트가 모두 0일때 0이라는 것을 알 수 있네요.


다음의 계산 예를 봅시다. 


      0111 1001

 OR 1000 1010

---------------------

      1111 1011



AND( & )

AND 연산자는 두 비트열 모두 1일때 1이 됩니다. 


0 & 0 = 0

0 & 1 = 0

1 & 0 = 0

1 & 1 = 1


둘 다 1일때만 1인것을 알 수 있습니다.


다음의 예를 보고 AND연산에 대해서 보도록 합시다.


        0001 1001

AND  0111 1000

----------------------

        0001 1000


OR연산과 AND 연산은 정말 쉽습니다.


XOR( ^ )

XOR 연산자는 두 비트열 중 1이 하나만 있을때 답이 1이 됩니다. 또는 1이 홀수개 일때 답이 1이 된다고 기억하면 됩니다.


0 ^ 0 = 0

0 ^ 1 = 1

1 ^ 0 = 1

1 ^ 1 = 0


1이 홀수개일때만 1이 됩니다.



       1001 1010

XOR 0110 1111

---------------------

       1111 0101



SHIFT( <<, >>)

쉬프트 연산은 비트를 옮기는 연산을 수행합니다. 옮기는 방향에 따라 두 종류가 있습니다. 바로 left shift와 right shift가 그것이죠.

비트를 왼쪽으로 옮기려면 << 연산을 사용하고, 오른쪽으로 옮기려면 >>연산을 사용합니다.




옮길 기준이 되는 비트열은 항상 연산자의 왼쪽, 얼만큼 옮길 건지를 결정하는 건 연산자의 오른쪽에 위치합니다.


만약 Left Shift 연산으로 왼쪽으로 비트열을 옮긴다면 가장 오른쪽에서부터 옮긴 비트열 길이까지 0으로 채워집니다.


하지만 Right Shift 연산으로 비트열을 오른쪽으로 옮기면 가장 상위비트(MSB:Most Significant Bit 라고 합니다.)를 왼쪽에서부터 채웁니다.



아래 예를 보면서 이해합시다.


0011 0110 << 3 = 1011 0000


왼쪽으로 3비트를 이동시키니 001은 삭제되었습니다. 그리고 10110이라는 비트열이 왼쪽으로 3비트 이동이 되지요. 그렇다면 자리가 3개가 남겠군요. 그건 0으로 채웁니다. 이것을 패딩(padding)이라고 하지요.


0011 1100 >> 3 = 0000 0111


이제 오른쪽 비트이동입니다. 우선 이 비트를 오른쪽으로 3비트를 옮기니 100이 삭제가 됩니다. 그후 나머지 4비트는 오른쪽으로 3비트를 이동하면 왼쪽에 3비트를 추가로 채워야하지요. 옮기기전 가장 상위비트(MSB)는 빨간색으로 표시된 0입니다. 그러니 오른쪽으로 이동할때는 이 MSB를 가지고 왼쪽비트를 채우니 000으로 채워지는 겁니다.


1001 1011 >> 4 = 1111 1001


자, 이제 MSB는 1입니다. 이 비트를 오른쪽으로 4비트를 이동하니 1001은 없어지겠죠? 그후 나머지 4비트를 오른쪽으로 이동하고 비워진 4비트는 MSB로 채우니 1로 채워지는 것입니다.


아! 여기서 2의 보수법을 모르시는 분을 위해서 잠시 간략하게 설명하겠습니다.


자료형에는 음수를 나타낼 수 있는 signed 타입와 unsigned 타입이 있습니다. 우리가 int, char와 같이 그냥 사용하게 된다면 default로 signed 자료형입니다. 하지만 음수를 표현할 필요가 없다면 unsigned라는 키워드를 붙여 unsigned int와 같이 표현합니다.

이제 음수를 나타낼 수 있는 signed 자료형에 대해서 음수를 표현하는 방법을 설명하겠습니다.


signed자료형에서 컴퓨터는 MSB에 따라 음수인지 양수인지 구별하는데요.

0이면 양수, 1이면 음수를 나타냅니다.

만약 위의 예에서 0011 1100은 MSB가 0이므로 양수입니다. 

그러니 정수로 표현하면 MSB를 제외하고 32+16+8+4로 70이 됩니다.


하지만 위의 예처럼 1111 1001은 MSB가 1이므로 음수입니다. 이때 2의 보수를 사용하는데요. 방법은 이렇습니다.

1) MSB를 제외하고 비트를 반전시킨다(이것이 1의 보수법입니다.)

2) 1을 더해준다. (1을 더해주는 것이 2의 보수법입니다.)


1)+2)의 방법에 따라 1111 1001의 음수값을 구해보면

   1000 0110 (1111 1001의 반전)

+ 0000 0001 (1을 더해주는 2의 보수)

--------------------

   1000 0111


이렇게 7이 나오게 되지요. 이때 MSB가 여전히 1이므로 -7이 되는 것이죠.





다음의 코드는 비트 연산에 대한 해답을 제공하는 프로그램입니다. 


#include <stdio.h>
#define SIZE 8

void printBitArray(char bits) {
	
	int i;
	for (i = SIZE-1; i >= 0;i--) {
		char bit = ((1 << i)&bits) > 0 ? '1' : '0';
		printf("%c ", bit);
	}
	printf("\n");
}
int main() {

	char bits1=0b10110110;
	char bits2=0b01101101;
	printf("bits1:"); printBitArray(bits1);
	printf("bits2:"); printBitArray(bits2);
	printf("\n\n");

	printf("NOT bits1:");  printBitArray(~bits1);
	printf("bits1 | bits2 :");  printBitArray(bits1 | bits2);
	printf("bits1 & bits2 :");  printBitArray(bits1 & bits2);
	printf("bits1 ^ bits2 :");  printBitArray(bits1 ^ bits2);
	
	printf("bits1 << 4 :");  printBitArray(bits1 << 4);
	printf("bits2 >> 2 :");  printBitArray(bits2 >> 2);
	printf("bits1 >> 3 :"); printBitArray(bits1 >> 3);
	
}

bits1과 bits2를 바꿔서 실행해보고 printBitArray의 매개변수로 비트연산을 해서 답을 확인해 보세요.


여기까지 비트연산자에 대해서 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

C언어 문자열 함수

문자열을 다룰때 어떤 문자열 단위로 자르고 싶은 경우나 어떤 문자열에서 임의의 문자열을 찾고 싶은 경우가 있지 않았나요?

그 경우에 사용할 수 있는 문자열 함수를 소개하려고 합니다. 문자열 함수를 사용하기 위해서는 항상 string.h 헤더 파일을 include해야한다는 것을 잊지 마세요.


strtok

이 함수가 문자열을 어떤 문자열 기준으로 자르는 역할을 하는 함수입니다. 일단 함수의 원형을 보시죠.


char *strtok(char *str, const char *delimiters);


2개의 파라미터를 갖고 있죠.


- str : 우리가 어떤 문자열을 자를지 넘겨받는 매개변수입니다.

- delimiters: 구분자라고 합니다. 여기서 자를 기준을 결정하는 것이지요.


예를 들어 str이 "show_me_the_money"라고 합시다. 그리고  문자열을 "_"(구분자)를 기준으로 자른다고 합시다. 그렇다면 show, me, the, money라는 4개의 문자열로 잘리겠죠.


- 반환값 : 잘린 문자열을 반환합니다. 만약 문자열이 전부 끝났다면 NULL을 반환하게 되지요.




이제 함수의 기본적인 설명은 여기까지하고 코드를 보면서 사용법을 확실히 알아보도록 하겠습니다.



strtok source code

#include <stdio.h>
#include <string.h>
int main() {
	
	char str[32] = "show_me_the_money";
	char *tok=strtok(str, "_");

	while (tok != NULL) {
		printf("token : %s\n", tok);
		tok = strtok(NULL, "_");
	}
	printf("기존 문자열 :%s\n", str);
}


우선 결과를 보고 왜 이런 결과가 나왔는지 알아보도록 하지요.


결과


token : show

token : me

token : the

token : money

기존 문자열 :show



이 코드에서는 위의 예와 마찬가지로 "show_me_the_money"라는 문자열을 자르고 있습니다.

strtok는 처음 str 매개변수에 NULL이 아닌 문자열을 사용하면 자를 문자열을 넘겨받은 문자열로 결정합니다.

이후 실행할때 str에 NULL을 전달하면 이전에 설정했던 문자열을 계속해서 자르는 것이죠.


그래서 반복문 while루프 안에서는 strtok에 str인자를 NULL로 넘겨주고 있는 것이죠. 잘 잘려지고 있기는 합니다.


하지만 마지막 줄을 보세요.

마지막 줄은 기존의 문자열 str을 출력하고 있는데 "show_me_the_money"가 출력되지 않고 "show"만 출력이 되고 있습니다. 왜 기존의 문자열인str[32]="show_me_the_money"가 출력이 되지 않는 것일까요?


strtok는 눈치채셨겠지만 자를 문자열을 변환시키면서 문자열을 잘라나갑니다.

우리는 문자열의 마지막 문자가 NULL문자로 끝난다는 것을 알고 있습니다. 그렇다면 마지막에 str이 "show"만을 출력했다는 것은 "show\0"가 된 것을 짐작할 수 있을까요?


"show"이후 문자는 바로 '_' 문자인데, '_'문자가 '\0'인 NULL문자로 바뀌게 된 것 아닐까요?

결론부터 얘기하자면 맞습니다. 우리는 이 한가지만 기억합시다.


문자열의 끝은 모두 '\0'(NULL) 문자로 끝이난다.



이거 하나만 기억하고 strtok가 어떻게 문자열을 자르게 되는지 그 과정을 살펴보도록 합시다.


우선 str이라는 문자열은 다음과 같이 메모리에 잡혀있을 겁니다.





이제 strtok(str,"_")를 호출하는 순간 str에서 "_"라는 문자열이 나올때 그 문자열 자리를 \0로 채우게 됩니다. 그 뒤에 ptr을 반환하게 됩니다. 바로 str[0]의 주소지요.


ptr은 위의 코딩에서 tok가 넘겨받게 되지요. 그래서 tok는 \0까지를 문자열로 인식하게 되므로 처음에는 "show"가 출력되게 되는 것이죠.




이후 ptr을 '\0'다음으로 위치시킵니다. 또 "_"가 나오면 그 자리를 NULL문자로 채우고 ptr의 주소를 반환합니다. 그렇다면 str[5]의 주소가 되겠지요.




이 후 ptr을 str[8]자리로 위치시킵니다. 이 자리는 '\0' 다음 위치지요. 다음에 나오는 "_"를 NULL로 채운 후 ptr을 반환시킵니다.




이제 '\0' 이후에 ptr을 위치시켜 다음 "_"를 찾는데 이제 "_"를 찾을 수 없고 '\0'문자를 만나게 되니까 "money"만을 출력하게 되는 것이죠. 




이 후에는 문자열이 종료되었으므로 strtok는 NULL을 반환하고 while반복문은 종료가 됩니다.


그렇다면 이제 다음 드는 의문은 strtok는 어떻게 ptr의 주소를 기억하고 있을까라는 점입니다. 그런 의문 안드세요?

왜냐면 함수는 종료가 되면 모든 지역변수를 반환하게 되는데 어떻게 ptr이라는 변수는 기억하고 있을까요?

바로 지역변수가 아니기 때문입니다. 변수나 자료형, 메모리 공간을 충분히 알고 있다면 ptr은 정적변수로 선언이 되었다는 것을 눈치챘을 겁니다. 그렇기 때문에 함수가 종료되어도 ptr은 다음 자를 문자열의 주소를 기억하고 있는 겁니다.




제가 한 설명이 의심이 된다면 한번 실험을 해보는 것도 나쁘지 않습니다.

다음의 코드를 실행시켜보세요.


strtok source code2

#include <stdio.h>
#include <string.h>

int main() {

	char str[32] = "show_me_the_money";
	int len = strlen(str);
	int i;
	char *tok;

	for (i = 0; i < len; i++)
		printf("'%c' : str[%d]의 주소:%p\n", str[i], i, &str[i]);
	printf("\n");

	tok = strtok(str, "_");
	while (tok != NULL) {
		printf("token : %s, address:%p\n", tok,tok);
		tok = strtok(NULL, "_");
	}
	printf("\n");

}


만일 제 설명이 맞다면 str을 자른 tok의 주소들이 "_" 이후의 주소들과 같을 겁니다. 왜냐면 "_"이후가 바로 자른 문자열의 시작주소이기 때문이죠.


결과를 보면서 확인해보세요.


결과

token : show, address:008FFC68

token : me, address:008FFC6D

token : the, address:008FFC70

token : money, address:008FFC74


's' : str[0]의 주소:008FFC68

'h' : str[1]의 주소:008FFC69

'o' : str[2]의 주소:008FFC6A

'w' : str[3]의 주소:008FFC6B

' ' : str[4]의 주소:008FFC6C

'm' : str[5]의 주소:008FFC6D

'e' : str[6]의 주소:008FFC6E

' ' : str[7]의 주소:008FFC6F

't' : str[8]의 주소:008FFC70

'h' : str[9]의 주소:008FFC71

'e' : str[10]의 주소:008FFC72

' ' : str[11]의 주소:008FFC73

'm' : str[12]의 주소:008FFC74

'o' : str[13]의 주소:008FFC75

'n' : str[14]의 주소:008FFC76

'e' : str[15]의 주소:008FFC77

'y' : str[16]의 주소:008FFC78



strstr

문자열에서 임의의 문자열을 찾을 수 있는 함수가 string.h에 존재합니다. 바로 strstr이라는 함수이지요.

char *strstr( char *str1, const char *str2);


- str1 : 전체 문자열을 의미합니다. str1이 이제 문자열을 찾을 대상이 되지요.

- str2 : 찾을 문자열을 의미합니다. 이 문자열을 str1에서 찾는 것입니다.


반환값 : str1에서 str2를 찾는다면 그 시작주소를 반환하게 됩니다. 찾지못하면 NULL을 반환합니다.


이제 예제를 보면서 함수를 어떻게 사용하는지 보도록 하지요.


▼strstr source code

#include <stdio.h>
#include <string.h>
int main() {

	char str[64] = "When I was young, I was ugly. But now, I'm still ugly";
	char *word = "ugly";
	char *ptr = strstr(str, word);
	int jump = strlen(word);
	int found = 0;
	while (ptr != NULL) {
		printf("%s\n", ptr);
		ptr = strstr(ptr + jump, word);
		found++;
	}

	printf("단어 갯수 :%d\n", found);
}

위의 코드는 str이라는 문자열에서 word라는 문자열을 찾습니다. 한번만 찾는게 아니고 계속해서 찾는거죠.
그러기 위해서 만약 단어를 찾으면 그 다음부터 찾아야하죠. 물론 ptr+1로 그냥 바로 다음 문자부터 찾으면 되겠지만 조금 더 많이 건너 뛰기 위해서 jump라는 변수를 사용한것 뿐입니다. 




그리고 found는 str에 그 word가 몇개나 존재하는지 알려줍니다.

아차, strstr 역시 str의 문자열 중 word와 일치한다면 일치한 str의 시작 주소를 넘겨주게 됩니다.
못 믿겠으면 직접 실험해보도록 하세요.

이제 결과를 보면서 확인해보세요.

결과

ugly. But now, I'm still ugly

ugly

단어 갯수 :2



여기까지 문자열 처리함수를 2개나 알아보았는데요. 물론 저의 설명이 허접해서 이해를 못하는 부분이 있을 수 있으니, 모르면 그냥 외워서 사용하도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,

우리는 난수를 사용하고 싶을 때가 있습니다. 하지만 이런 기능을 만드는 것은 좀처럼 쉬운 일은 아니지요. 귀찮기도 하구요.

C언어에서는 그러한 프로그래머를 위해서 난수생성함수를 제공하고 있습니다. 바로 rand라는 함수이지요. 외우기도 쉽네요. rand(om)으로 기억하면 되니까요.

 

rand

rand함수를 사용하기 위해서는 stdlib.h 헤더파일을 include해야합니다. rand함수는 0부터 RAND_MAX까지 범위까지 난수를 생성합니다. 함수 원형을 같이보시죠.

#include <stdlib.h>

int rand(void);

보시는 바와 같이 rand함수는 int형을 반환하게 됩니다. 아하, 그러면 rand함수를 쓰게 되면 랜덤인 정수형이 나오겠구나. 알 수 있죠?

이제 이 함수를 이용해서 1부터 100까지 정수 중 10개의 수를 랜덤하게 뽑아내는 프로그램을 짜보도록 하지요.

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

int main() { 
        int i; 
        for (i = 1; i <= 10; i++) 
                printf("%d ", (rand() % 100) + 1); 
        printf("\n");
}
이제 결과를 보도록 할까요??
 
42 68 35 1 70 25 79 59 63 65

 

 

오 랜덤하게 실행이 되는 군요. 100가지의 숫자 중 랜덤한 10개의 숫자를 뽑아냈습니다.

저는 신기해서 한번 더 실행해보겠습니다.

 

42 68 35 1 70 25 79 59 63 65

????

프로그램을 실행할때마다 바뀌지 않는데요? 우리는 이런 랜덤한 값을 원한게 아닙니다. 우리는 프로그램을 실행할때마다 랜덤하게 10개의 수를 출력하는 프로그램을 원하는 건데요. 지금 출력된 것은 단지 일정한 숫자 배열을 출력한 것과 같다고 느껴집니다.

왜 C언어는 우리에게 이런 사기를 치는 것일까요?

 

srand

사실 rand함수는 srand함수에 의존적입니다. srand의 s는 seed라는 뜻으로 이 seed값에 따라 rand의 값이 바뀌게 됩니다. srand는 rand함수와 같이 stdlib.h 헤더파일에 존재합니다.

만일 이 함수를 호출하지 않고 rand함수를 호출한다면 srand(1)을 호출하고 rand함수를 호출한 효과와 같습니다.

함수의 원형은 다음과 같은데요.

#include <stdlib.h>

void srand(unsigned int seed);

양의 정수만 seed로 사용할 수 있습니다. 그렇다면 우리가 srand의 seed값을 2로 주면 위의 결과와 다를까요?

위의 코드에서 for로프 위에 srand(2)를 추가해보세요.

46 17 99 96 85 51 91 32 6 17

 

아까와는 다른 결과를 볼 수 있군요. srand에서 seed를 바꿔서 실행시켜보세요. 나오는 값이 계속 달라짐을 알 수 있습니다. seed값만 바꿔주면 그 seed값에 따라 값을 랜덤하게 뽑아 올 수 있군요.

 

하지만 이것 마저도 아직 우리를 만족시킬 수가 없습니다. 이렇게 되면 프로그램을 실행시킬때마다 seed값을 바꾸고 다시 컴파일하는 과정을 거쳐야하기 때문이죠. 우리는 이런 허접한 코드는 쓰지 말도록 합시다.

 

우리는 이것보다 더 잘할 수 있습니다. 잘 할 수 있고 말고요. 한번 생각해봅시다. 프로그램 실행시 계속 바뀌는 값은 뭐가 있을까요?

주소값? (사실 제가 랜덤함수를 구현한다고 생각해볼때 고려해봤던 것 중 하나입니다.)

바로 시간입니다. 시간은 지금 이 순간에도 항상 바뀌고 있지요. 그래서 소개할 다음 함수가 time이라는 함수입니다.

time

time함수는 이름 그대로 시간에 대한 정보를 얻어오는 함수랍니다. 우선 time함수를 사용하기 위해서는 time.h라는 헤더파일을 include해야하지요.

함수의 원형을 한번 살펴볼까요?

#include <time.h>

time_t time(time_t *tloc);

 

이 함수는 1970년 1월 1일 0시 (UTC)부터 현재까지 흐른 시간을 반환합니다. 반환은 하지만 그 시간이 초단위입니다. 만일 우리가 현재까지 흐른 시간을 구하려면 만약 timeptr에 NULL을 전달하고 반환값을 받거나, 아니면 timeptr에 인자를 전달해서 현재까지 흐른 시간을 초단위로 받을 수 있습니다.

이제 아주 기본적인 사용법을 알게 됐으니 코드로 구현하도록 해봅시다. 

 

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

int main() { 
        int i; 
        srand(time(NULL));

        for (i = 1; i <= 10; i++)  
                printf("%d ", (rand() % 100) + 1);
        printf("\n");
}

 

단지 srand의 인자를 time(NULL)로 바꾼거 밖에 없습니다. time(NULL)을 호출하면 1970/1/1 0시부터 현재(프로그램 실행 시)까지 흐른 시간을 return한다고 했지요? 그러니 이 프로그램을 실행할때마다 srand의 seed값이 바뀌게 되는 겁니다.

이제 확인을 해봅시다.

첫번째 실행

79 61 20 69 3 67 82 24 63 35

두번째 실행

44 53 56 15 86 98 95 14 15 46

어떻습니까? 이제 이 프로그램을 여러번 실행해도 값이 다르게 나온다는 것을 알 수 있습니다.

 

그렇다면 우리가 랜덤한 값을 얻고자 할때는 rand, srand, time함수를 전부 다 써야하나요?

네, 이 세가지 함수들은 묶어서 기억하셔야합니다. 사용법은 어렵지 안잖아요 그쵸??

 

이상으로 여기까지 C언어에서 난수를 생성하는 쉬운 방법을 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,