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

 

1014번: 컨닝

문제 최백준은 서강대학교에서 “컨닝의 기술”이라는 과목을 가르치고 있다. 이 과목은 상당히 까다롭기로 정평이 나있기 때문에, 몇몇 학생들은 시험을 보는 도중에 다른 사람의 답지를 베끼려 한다. 시험은 N행 * M열 크기의 직사각형 교실에서 이루어진다. 교실은 1*1 크기의 단위 정사각형으로 이루어져 있는데, 각 단위 정사각형은 자리 하나를 의미한다. 최백준은 컨닝을 방지하기 위해서 다음과 같은 전략을 세웠다. 모든 학생은 자신의 왼쪽, 오른쪽, 왼쪽 대각

www.acmicpc.net

문제 소개

백준 1014번 문제 컨닝에 대한 풀이 포스팅입니다. 사실 이 문제는 컨닝2 라는 문제도 있고, 컨닝2의 경우 컨닝1과 문제는 동일하나 N,M값이 다릅니다.

 

컨닝1은 N,M이 최대 10이고, 컨닝2는 N,M이 최대 80입니다.

 

일단 이 포스팅에서는 컨닝1을 풀이하기 위한 내용을 소개합니다.

 

사실 저는 이 문제에 대한 풀이 방법(카테고리 정도)를 알고 문제를 접근했습니다.

 

약간 스포일러를 하자면 다이나믹 프로그래밍으로 풀 수 있고 거기에 추가적인 기법이 하나 들어가게 됩니다.

 

다이나믹 프로그래밍 + 비트마스킹

일반적인 다이나믹 프로그래밍은 많이 들어보셨을 것이지만, 비트마스킹을 조합해서 쓰는 것은 잘 모르시는 분들도 있을 것입니다.

 

일단 문제를 분석해보면, 다이나믹 프로그래밍으로 풀만한 최적 부분구조(Optimal substructure)와 또 하나 특성이 있는데 그것이 성립하는 것 같습니다.

 

최적 부분구조라 함은, 부분문제의 최적값이 전체 문제의 최적값을 구하는데 사용이 된다는 것이고, 다른 하나의 특성은 부분문제의 풀이가 다른 나머지 문제를 풀 때 영향을 주지 않는다는 것이지요.

 

사실 영향을 주긴 하는데, 컨닝 문제 조건 상 앞에 줄에 사람을 어떻게 배치했느냐가 바로 뒷 줄에 사람을 배치하는 것에만 영향을 줍니다. 따라서 바로 앞줄에 사람을 어떻게 배치했는지를 저장을 할 필요가 있습니다.

 

그리고 앞에서 부터 사람을 채운다고 하면 앞에서 사람을 많이 채워놓아야지 뒤에서도 전체 사람 수를 채우는 데 도움이 되므로 최적 부분구조가 성립한다고 할 수 있겠지요.

 

그런데 사람을 배치하는 것은 한 줄(row)단위로 생각하면 사람이 있다, 없다의 2진수로 표현이 가능하게 됩니다.

 

컴퓨터에서는 정수를 2진수로 표현하기 때문에 이를 이용해서 앞 줄의 사람이 앉아있는 상태를 정수 하나로 쉽게 표현할 수 있을 것 같습니다.

 

이러한 방식을 bitmasking이라고 합니다.

 

점화식 정의

일단 사람을 앞에서 부터 채워야지, 이전에 앞에 채운 사람의 형태에 따라 뒤에서 valid한 사람 배치, invalid한 사람 배치를 알 수 있으므로 for-loop를 이용한 동적계획법으로 풀이를 하려고 설계를 해 보겠습니다.

 

그러면 점화식을 정의를 해 보아야 하는데 다음과 같습니다.

//(dp[i][j]//)

여기서 i는 i번째 열까지 채웠을때의 상태를 나타내고, j는 해당 i번째 열에 사람을 어떻게 배치했느냐를 나타냅니다.

즉 점화식의 정의는 i번째 열까지 사람을 다 채우고, i번째 열에는 사람을 j의 비트 모양으로 배치했을 때 가장 많이 배치할 수 있는 사람의 수라고 보시면 됩니다.

 

그러면 //(j//)모양으로 사람을 배치한다는 것은 무슨 말이 될까요?

 

간단하게 //(j=5//)라고 한다면 5는 2진수로 표현하면 101이 됩니다. 그러면 1이 있는 자리에는 사람이 있는 것이고, 0이 있는 자리에는 사람이 없는 것입니다. 이러한 방식으로 사람을 앉힌다는 것이지요.

 

구현전략

그러면 이러한 알고리즘으로 풀이 코드를 구현해보도록 하겠습니다.

 

그런데 보면 학생은 양 옆의 자리를 컨닝할 수 있으므로, 바로 인접하여 2명의 학생이 옆으로 배열되는 경우는 없습니다.

따라서 bit 들 중에서 인접한 2개의 bit가 같이 1로 되어있는 경우는 제거를 합니다. 그리고 이 bit 집합들은 자주 쓰일 것 같으므로 한번 만든 뒤 계속 사용하도록 하겠습니다.

 

그리고 비트마스킹의 장점 중 하나가, 모든 비트 경우를 확인하는 것은 단지 for-loop하나를 돌리면 다 확인이 가능합니다.

 

이러한 전략들을 적용하여 코드를 작성해보도록 하겠습니다.

 

풀이 코드

 

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef pair<int, int> pii;
string field[15];
int dp[15][1025]; // dp[i][j]=> i번째 줄에 j비트 모양으로 학생을 배치할때, i번째까지 사람을 앉힐 수 있는 최대수
bool seat_check(string& seats, int bit) {
	for (size_t i = 0; i < seats.length(); i++) {
		if (seats[i] == 'x' && (bit & (1 << i))) return false;
	}
	return true;
}
bool adj_check(int bit, int width) {
	for (int i = 0; i < width - 1; i++) {
		int val = (3 << i);
		if ((bit & val) == val) return false;
	}
	return true;
}
bool bits_check(int bit, int fbit, int width) {
	//앞자리랑 뒷자리 관계가 제대로 성립하는지 확인
	for (int i = 0; i < width; i++) {
		if ((1 << i) & fbit) { //앞자리에 앉는 녀석이 있을 때
			if (i > 0 && ((1 << (i - 1)) & bit)) return false; //왼쪽에 뒤에 앉는 경우
			if ((1 << (i + 1) & bit)) return false; //오른쪽 뒤에 앉는 경우
		}
	}
	return true;
}
void solve() {
	int ans = 0;
	memset(dp, 0, sizeof(dp));
	int n, m; cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> field[i];
	}
	vector<pii> bits_set; //인접한 자리가 아닌 비트 마스크들을 미리 만들어놓음.
	for (int bit = 0; bit < (1 << m); bit++) {
		if (adj_check(bit, m)) {
			int cnt = 0;
			for (int i = 0; i < m; i++) {
				if ((1 << i) & bit) cnt++;
			}
			bits_set.push_back(make_pair(bit, cnt));
		}
	}
	for (int i = 1; i <= n; i++) {
		//i 번째 줄을 채울 예정
		for (pii bit : bits_set) {
			if (!seat_check(field[i], bit.first)) continue; //부서진 자리에 앉으려고 하는 경우
			for (pii fbit : bits_set) { //앞자리 앉는 녀석
				if (bits_check(bit.first, fbit.first, m)) {
					dp[i][bit.first] = max(dp[i][bit.first], dp[i - 1][fbit.first] + bit.second);
					ans = max(ans, dp[i][bit.first]);
				}
			}
		}
	}
	cout << ans << endl;
	return;
}
int main() {
	int tc; cin >> tc;
	while (tc--) solve();
}

이번 포스팅에서는 인코딩과 암호화 및 해시에 대하여 설명해보고자 한다.

자주 혼동되는 개념이기도 하고 처음에 접하면서 햇갈려하는 사람들이 꽤 있어서 명확하게 구분을 지어보려고 한다.

 

인코딩(Encoding)과 디코딩(Decoding)이란?

일단 인코딩에 대하여 이야기해보겠다. 인코딩(encoding)은 code화 한다는 뜻이다. 무언가를 코드로 바꾼다는 뜻이다.

반대로 디코딩(decoding)은 코드에서 원래 모습으로 되돌리는 것이다.

 

약간 햇갈리는 개념이 될 수 있는데, 이 단락은 이해가 안되면 그냥 넘어가도 좋다.

인코딩이란 심볼을 코드로 바꾸는 것이고, 디코딩은 코드를 심볼로 다시 바꾸는 과정이다.

여기서 심볼은 뜻을 가진 무언가라고 보면 되고, 코드는 뜻을 갖지 않은 무언가이다.

즉, 인코딩은 의미(내용, 혹은 뜻)을 기호로 바꾸는 과정, 디코딩은 그 반대의 과정이다.

 

그렇다면 코드(code)는 무엇일까?

 

코드는 인코딩 방법에 따라 각자 다른데, 여기서는 무언가를 나타내는 기호라고 생각하면 되겠다.

인코딩 방법에 따라서 각각의 코드가 무엇을 나타내는지는 달라지게 된다.

 

사실 컴퓨팅, 계산과학 등 컴퓨터와 관련된 분야에서 심볼과 코드의 경우, 보통 사람이 읽을 수 있는 내용들(심볼)을 컴퓨터가 인식할 수 있는 데이터(이진 데이터, bit)로 변화하는 경우가 대부분이다.

따라서 심볼은 자연언어(영어, 한국어, 일본어 등등...)이거나 이미지(사람이 인식할 수 있는) 및 동영상, 소리 등이 되는 경우가 많고, 코드는 컴퓨터가 인식할 수 있는 바이너리 데이터가 되곤 한다.

 

모스 부호(Morse code)를 예로 들어보겠다. 모스 부호의 영어 단어도 Morse Code이다.

이 경우에는 알파뱃(A,B,C,...,Z)과 아라비안 숫자(0,1,2,3,...,9)가 심볼이 되고, 오른쪽에 점과 작대기가 코드가 된다.

점과 작대기는 사실 뜻이 없고, 알파뱃은 뜻이 있는데, 이 알파뱃들을 점과 작대기로 바꾸는 과정은 인코딩이 되고, 점과 작대기로 알파뱃으로 되돌리는 과정은 디코딩이 된다.

 

사실 이제 눈치채었겠지만, 인코딩과 디코딩 과정은 표현방식을 바꾸는 것 뿐이다. 그리고 그 과정에서 정보의 손실은 없다.

정보의 손실이 없다고 하는 것은 어떤 정보 A를 B로 인코딩 한 뒤, 다시 디코딩 했을 때 원래 정보 A에서 사라지는 정보가 없다는 것이다.

 

만약 어떤 영어 문장이 있었을 때, 이 문장에서 짝수번째 자리에 있는 알파뱃만을 모스부호 인코딩을 한 경우, 이 과정에서는 정보의 손실이 나타나게 된다.

왜냐면 다시 모스부호 디코딩을 한 경우 문장의 홀수번째 자리에 있는 알파뱃만이 남아있기 때문에, 짝수번째 자리에 있던 알파뱃의 정보는 손실이 되기 때문이다.

 

다른 유명한 인코딩은 무엇이 있을까?

다양한 인코딩들이 있다. 사실 인코딩은 심볼을 어떻게 코드화 하는지, 그리고 되돌리는지에 대한 규칙만 있으면 쉽게 만들 수 있다. 그래서 인코딩 이름에 encoding이 들어가는 경우도 있고, 그냥 무슨 무슨 코드라고 써있는 경우도 있다.

간단하게 봐서는 심볼과 코드의 단순 1대 1 대응관계라고도 볼 수 있다.

 

아스키 코드(ASCII Code)

아마 컴퓨터공학을 전공하면 처음으로 배운 코드 중 하나일 것이다. 로마자 알파뱃과 아라비아숫자, 그리고 일부 특수문자들을 1byte로 표현할 수 있는 간단한 코드이다. 이 문자들은 1byte 정수로 인코딩되며, C언어에서 문자(열)을 다룬다면 매우 친숙한 코드일 것이다.

유니코드 (UTF-8 혹은 UTF-16 등)

아스키코드의 경우 로마자 알파뱃만 표현이 가능하다는 불편한점을 개선한 코드로, 전 세계 모든 글자를 다 표현할 수 있는 문자 처리 방식이다. 사실 유니코드 자체는 인코딩이 아닌, 이런 전세계 모든 문자 각각을 1바이트 이상의 크기로 표현할 수 있게하는 방식이고 이를 실제로 구현한 인코딩이 몇 가지가 있는데 그 중 가장 유명한 녀석 중 UTF-8와 UTF-16이 있다.

Base 64 인코딩

Base 2라고 하면 2진법을 뜻하고 base 10이라고 하면 10진법을 뜻한다. 따라서 base64는 직역하면 64진법이 된다.(64진법이라고 부르는 경우는 없는데 이해를 돕기 위한 내용이다.)실제로도 64개의 서로 다른 printable한 문자들을 코드로 사용하고 있으며, 심볼로는 모든 범위의 바이너리 값을 표현할 수 있도록 고안된 인코딩 방법이다.

이미지나 동영상 등 바이너리 데이터를 깨지지 않는 출력가능한 문자로 표현하기 위해 자주 사용되는 녀석 중 하나이다.

uuencoding

요녀석은 자주 나타나지는 않는데 이 녀석도 바이너리 데이터를 printable 한 값으로 변환시킬 수 있는 인코딩방법이다.

URI Encoding(% encoding)

URI에서 일부 특수문자들 등을 나타낼 때 사용하는 인코딩 방법으로, 인코딩 방식이 % 뒤에 16진수 hex 값 2글자가 들어가는 방식의 인코딩이라서 퍼센트 인코딩이라고도 부른다. 네이버 주소창에서 아무 한글로 검색한 뒤, URL창을 보면 마지막 URI 쿼리스트링 부분에 %로 인코딩 된 부분들을 쉽게 볼 수 있다.

 

인코딩은 왜 하는 것일까?

인코딩을 하는 이유는 상황별로 조금 다를 수 있긴 하다. 하지만 일단 컴퓨터라는 것 자체가 0과 1로 이루어진 bit들이 모여서 각종 작업을 하는데, 이때 한글이나 영어와 같은 데이터들을 처리해야하는 경우가 항상 있다. 그러면 컴퓨터는 어차피 숫자만 다루기 때문에, 한글과 영어와 같은 심볼들을 숫자 Code로 맵핑해야 한다.

이런 경우 문자열 인코딩(Character encoding)이 필연적으로 필요해진다.

 

또한 어떤 값들을 네트워크나 다른 영역으로 전송해야 하거나 전송받아야 하는 경우, 서로 다른 플랫폼이라서 데이터를 표현하는 방식이 다르더라도 서로 호환되도록 공통적인 데이터 표현방식이 필요할 수 있다. 마치 세계 공용어로 영어가 쓰이면 지구 모든 사람이 영어를 할 줄 안다면 세계 누구와도 언어가 통할 수 있듯이, 그러한 표준화된 데이터 표현 방법을 정의한다고 보면 된다.

 

따라서 인코딩의 목적은 정보의 호환성이 되겠다. 

 

암호화(encrypt)와 복호화(decrypt)란?

인코딩과 암호화를 많이 햇갈려하곤 한다. 그런데 둘은 목적 자체도 다르고 성질도 다르다. 인코딩은 표현 방법을 바꾸는 것이고, 암호화는 남이 정보를 알아보지 못하게 하는 것이다.

암호화의 목적

암호화의 목적은 남들이 정보를 알아보지 못하게 만드는 것이다. 평문(plain text)을 암호화하면 암호문(cipher text)이 되고, 암호문을 복호화하면 평문이 된다.

그리고 여기서 '남'이란 암호 키가 없는 사람이다. 암호 키가 있는 사람은 복호화를 할 수 있고, 암호키가 없는 사람은 복호화를 할 수 없다. 즉, 키가 없으면 원래 평문이 어떤 값인지 알 수 없다는 것이다.

 

여기서 인코딩과 암호화의 첫번째 차이가 나왔다. 바로 '키'의 존재 유무이다.

 

암호의 종류

암호 부분은 자세히 들어가면 굉장히 복잡하다. 보통 암호를 금고에 많이 비유를 하는데, 일단 암호 시스템의 구성요소는 4가지라고 보면 된다.

평문과 암호 알고리즘 그리고 키와 암호문이 된다.

 

평문을 암호 알고리즘과 키를 통해서 암호화를 하면 암호문이 된다.

 

평문은 중요 정보라고 보면 되고, 암호 알고리즘은 금고이다. 그리고 키는 금고의 패스워드가 된다.

금고안에 들어간 중요 정보는 암호문이라고 보면 된다.

 

암호는 키의 형태에 따라 대칭키, 비밀키 암호가 있고, 대칭키는 암호화에 쓰이는 키가 복호화에도 쓰이는 것이다.

비밀키는 키가 비밀키 및 공개키로 한 쌍이 있고, 비밀키로 암호화하면 공개키로 복호화가 되고, 공개키로 암호화를 하면 비밀키로 복호화가 되는 특이한 방식의 암호이다.

 

암호 알고리즘의 종류

암호 알고리즘도 다양한 종류가 있는데, 유명한 몇 가지를 알아보도록 하겠다.

 

DES(Data Encryption Standard)

매우 오래된 대칭키 암호 알고리즘이다. 취약성하다고 알려져서 요즘은 그냥은 사용하지 않는다.

 

AES(Advanced Encryption Standard)

DES가 취약하다고 알려져서 이를 대체하기 위해 새로이 만들어진 대칭키 암호 알고리즘이다.

 

RSA(Rivest Shamir Adelman)

암호 알고리즘을 만든 3명의 이름을 본 따서 이름을 지은 공개키 암호 알고리즘이다. 지금까지도 널리 잘 쓰이고 있는 암호 알고리즘이며, 공개키 암호 알고리즘의 대표격이다.

 

암호화와 정보의 손실

아까 인코딩은 정보의 손실이 없다고 했는데, 그렇다면 암호화는 정보의 손실이 있을까?

답은 구현에 따라 다르다이다. 단방향 해시함수를 이용해서 암호화를 하는 경우가 있는데, 이 경우는 해시함수의 특성상 정보 손실이 일어나고 복호화가 불가능하다.

 이는 조금 특별한 경우인데, 보통 계정 정보의 패스워드를 이 방식을 통해서 암호화하여 저장한다.

패스워드가 동일한지는 입력한 패스워드를 단방향 해시함수로 암호화 한 뒤, 기존에 저장된 암호문과 일치하는지를 확인한다.

 

 

 

 

 

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

https://ko.wikipedia.org/wiki/%EB%AA%A8%EC%8A%A4_%EB%B6%80%ED%98%B8

백준에서 문제를 풀어보다가 LIS 시리즈물들이 있길래 이 김에 공부할 겸 정리도 한번 해보려고 합니다.

PS 분야에서는 꽤나 유명한, 매우 well-known한 주제이기도 합니다.

 

백준에는 무려 6문제나 있지요. 사실 뒤에 숫자는 레벨이라고 봐도 됩니다. 고로 2번 문제 난이도 < 3번 문제 난이도 < 4번 문제 난이도... 뭐 이런식이지요.

 

백준 11053번 문제 "가장 긴 증가하는 부분 수열"

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

문제부터 한번 보겠습니다. 첫번째 문제입니다.

 

최장 증가 부분 수열

일단 Longest Increasing Subsequence가 무엇인지를 좀 알고 넘어가면 좋을 듯 합니다.

사실 부분수열이면 subsequence이지요. 그렇다면 수열은 sequence라고 보시면 됩니다.

 

subsequence라고 하는 녀석은 연속되지 않아도 됩니다. 이 부분이 간단하지만 핵심적인 내용인데, (반면 substring은 연속되어야 합니다.)

위 문제에서 예시를 보면, 10 20 30 50이 최장 증가수열입니다.

 

하지만 원본 배열에서는 20과 30 사이에 10이 들어있어서 20과 30은 연속되지 않습니다. 그래도 subsequence가 맞다는 것이지요.

 

그러면 길이가 N인 배열의 subsequence의 개수는 //(2^N//)개가 되겠네요. 각각의 원소가 부분 수열에 포함되거나, 그렇지 않거나 하니깐요.

 

그러므로 Brute force로 문제를 풀려고 한다면 당연히 시간 복잡도는 //(O(2^N)//)가 나오게 됩니다.

 

문제에서 주어진 //(N=1000//)이므로 완전탐색으로는 풀 수 없겠지요.

 

그렇다면 어떻게 풀 수 있을까요?

 

동적 계획법 풀이 (Dynamic Programming)

LIS는 유명한 동적계획법 문제 중 하나이기도 합니다. 1차원 다이나믹 프로그래밍으로 //(O(N^2)//)의 시간복잡도로 문제를 해결할 수 있습니다.

 

간단하게 점화식을 새운 뒤 부분 문제들을 풀면 됩니다.

 

헌데 문제 특성 상, 부분 문제인 이미 만든 Subsequence들을 저장을 해야 하는데, 리스트 형태로 되어 있으면 다루기가 힘들지요.

 

그래서 subsequence의 마지막 값만을 이용해서 점화식을 정합니다.

 

//(dp[i]//)는 //(i//)번째 원소를 부분 수열의 마지막으로 하는 수열 중 가장 긴 수열의 길이

로 정의합니다.

 

#include <bits/stdc++.h>
using namespace std;

int dp[1001]; //dp[i] -> i번째 값을 마지막으로 하는 subsequence중 가장 긴 녀석의 길이
int arr[1001];
int main() {
    int n;
    cin >> n;
    for (int i=0; i< n; i++) {
        cin >> arr[i];
        dp[i] = 1; //길이가 1인 경우로 초기화
    }
    for (int i=0; i < n; i++) {
        int& last_val = arr[i];
        for (int j=0; j < i; j++) {
            int& target_val = arr[j];
            if (last_val > target_val) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    
    int ans = 0;
    for (int i=0; i<n; i++) {
        ans = max(ans, dp[i]);
    }
    cout << ans << '\n';
	return 0;
}

 

위와 같이 구현해서 1번문제를 가볍게 풀 수 있습니다.

 

백준 12015번 문제 "가장 긴 증가하는 부분 수열 2"

그러면 다음 문제를 한번 보도록 합시다.

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

 

이전 문제와 달라진 점은 N의 크기 뿐입니다. N이 100만이 되었으니, 기존의 //(O(N^2)//)의 알고리즘으로는 시간 내에 풀이가 힘들게 되었습니다.

 

좀 더 효율적인 알고리즘이 있을까요?

 

NlgN 풀이 - 바이너리 서치

//(O(NlgN)//)의 시간복잡도를 갖는 LIS 풀이로 2가지가 있는 것으로 알고 있는데, 하나는 세그먼트 트리를 이용한 방법이고 다른 하나는 이번에 다루어볼 바이너리 서치를 활용한 방법입니다.

 

아이디어

일단 배열 A = {2, 5, 3}이라고 가정해봅시다.

그러면 LIS가 2개가 나옵니다.

{2, 5}와 {2, 3}이지요.

 

그러면 여기서 A배열 뒤에 7과 11이 추가된다고 생각해보겠습니다. 그러면 A = {2, 5, 3, 7, 11}이 됩니다.

 

그리고 이렇게 새로 추가된 7과 11 덕에 기존의 LIS도 더 길어지게 됩니다.

{2, 5} => {2, 5, 7, 11}

{2, 3} => {2, 3, 7, 11}

 

이러한 식으로 알고리즘이 전개됩니다. 여기에 A 배열 마지막에 8이 추가된다면 어떻게 될까요?

A = {2, 5, 3, 7, 11, 8}이 되는데, 요 8녀석이 기존에 알려진 LIS들 사이에 끼일 수 있을까요?

7과 11은 기존 LIS의 마지막 값 보다 명백히 큰 값이라서 그냥 뒤에다가 붙이면 된다는게 확실한데, 8은 마지막 값인 11보다 작습니다.

 

그러면 이 8이라는 녀석을 버려야할까요?

 

LIS에서 그리디한 성질

사실 이런 경우 11을 8로 바꾸는 경우가 무조건 더 좋습니다.

 

일단 수열이 여기서 끝난다고 하면, {2,3,7,11}이나 {2,3,7,8}이나 Increasing subsequence의 길이는 둘 다 같습니다.

 

이후 뒤에 어떤 수가 더 온다고 한 들, 8로 끝난 경우가 11로 끝난 경우보다 더 긴 수열을 만드는데 있어서 유리하거나 같습니다.

 

일단 당장 뒤에 오는 숫자의 범위를 3가지로 나누어 보겠습니다.

 

1) 11보다 큰 수가 오는경우, 

2) 9이상 11이하의 수가 오는 경우

3) 8이하의 수가 오는 경우.

 

1번 11보다 큰 수가 오는 경우, 8로 끝난 수열이나 11로 끝난 수열이나 둘 다 취할 수 있습니다.

예를 들어 이후 13이 온 경우

{2,3,7,11,13}과 {2,3,7,8,13}이 가능한데, 둘 다 같은 길이에 마지막 수가 13으로 끝나는 수열이 됩니다.

 

2번 9이상 11이하의 수가 오는 경우 8로 끝난 수열은 길이가 1추가되는데, 11로 끝난 수열은 길이를 추가할 수가 없습니다. 또한 마지막 수가 11이하의 수 이므로 기존 11로 끝난 수 보다 무조건 유리합니다.

 

3번 8이하의 수의 경우는 둘 다 수열을 연장할 수 없으니 두개의 수열 모두 추가 수를 취할 수 없으니, 상관이 없지요.

 

따라서 같은 길이라면 마지막 수가 작을 수록, 이후에 더 긴 수열을 만들기 위해서는 더 유리하다고 볼 수 있습니다. 이러한 점에선 LIS가 그리디한 성질을 만족한다고 할 수 있겠습니다.

 

"수열의 마지막 수가 작을수록 더 긴 LIS를 만드는데 유리하다" 고 볼 수 있겠습니다.

 

이러한 점으로 다음과 같은 알고리즘을 한번 만들어 볼 수 있습니다.

알고리즘 설계

가장 긴 증가부분수열이 될 수 있는 녀석들을 Active list라고 부르도록 하겠습니다.

Increasing subsequence들 몇개들을 가지고 있으면서, longest가 될 수 있는 녀석을 알아내는 것인데,

위에서 알아낸 Greedy한 성질로 전체를 다 만들지 않고 추려낼 수가 있습니다.

 

같은 길이의 Increasing subsequence라면, 마지막 수가 가장 작은 녀석 하나만 알고 있어도 Longest IS를 구할 수 있다는 점입니다.

물론 i값을 0부터 N-1까지 증가시켜가면서 만드는 것이기 때문에 위치에 따른 dependency도 체크를 할 수있습니다.

 

알고리즘은 다음과 같습니다.

 

1. 만약 //(A[i]//)가 모든 Active list의 마지막 수 보다 작다면, 길이 1짜리 Active list를 하나 만든다.

2. 만약 //(A[i]//)가 모든 Active list의 마지막 수 보다 크다면, 가장 긴 Active list를 하나 복붙을 한 뒤, 마지막에 //(A[i]//)를 붙인다.

3. 만약 //(A[i]//)가 Active list 마지막 수들 중 가장 작은녀석 보다는 크고, 가장 큰 녀석 보다는 작다면, Active list 마지막 수 들 중에서 //(A[i]//)보다는 작은 녀석 중 가장 큰 리스트를 골라서, 그 리스트의 복사본 뒤에 //(A[i]//)를 붙인다.

 

끝까지 진행한 뒤, Active list중 가장 긴 녀석의 길이가 LIS의 길이가 됩니다.

 

{0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}

위에 있는 수열로 위 알고리즘을 진행해보면, 아래와 같이 나옵니다. geeksforgeeks 블로그의 값을 그대로 긁어왔습니다.

 

 

 

위와 같이 진행이 됩니다.

 

그러면 이 Active List들을 잘 가지고 있어야 하는데 어떤식으로 구현을 하면 좋을까요?

 

구현 전략

 

NlgN 풀이를 하려는 이유는 배열의 길이가 꽤 길기 때문에 시간복잡도를 줄이기 위해서입니다.

그런데 저러한 Active List를 다 가지고 있게 되면, //(O(N^2)//)의 공간 복잡도를 갖게 되는데 NlgN으로 푸는 시간복잡도를 갖는 경우 대충 N이 10만 ~ 100만 정도 될 터인데, 이러한 N값의 N 제곱의 공간 복잡도는

필요 메모리 양이 꽤 많아서 보통 모두 할당할 수 없습니다.

 

그래서 저 Active list 전체를 다 저장하는 것 외의 방법을 찾아야 합니다.

 

그런데 Active list들의 데이터 특징을 좀 살펴보면, 같은 길이의 active list는 앞서 알게 된 그리디한 성질 때문에 하나만 가지고 있습니다. 그리고 모든 active list의 길이는 1,2,3,4 와 같은 식으로 같은 길이의 리스트는 하나만 나오고

점진적으로 1만큼 차이가 나는 것을 알 수 있습니다.

 

따라서 Active list의 마지막 숫자만 가지고 있어도 될 것 같은 느낌이 듭니다.

그렇게 저장할 경우 //(O(N)//)의 공간복잡도를 갖는 배열 하나만 가지고 있어도 됩니다.

따라서 Active list의 마지막 숫자의 값만 가지고 있는 배열을 따로 만들도록 하겠습니다.

 

이 배열을 list라는 벡터로 저장하고 구현을 한 소스코드 내용입니다.

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef pair<int, int> pii;
#define MAXN 1000006
int arr[MAXN];
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		scanf("%d", arr + i);
	}
	vector<int> list;
	for (int i = 0; i < n; i++) {
		auto& a = arr[i];

		if (list.size() == 0) list.push_back(a);
		else if (a < list[0]) list[0] = a;
		else if (a > list.back()) list.push_back(a);
		else {
			auto it = upper_bound(list.begin(), list.end(), a - 1);
			*it = a;
		}
	}

	printf("%u\n", list.size());

	return 0;
}

 

이렇게해서 백준 12015번 문제인 가장 긴 증가하는 부분 수열 2 문제를 풀 수 있습니다.

 

 

 

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

http://en.wikipedia.org/wiki/Longest_increasing_subsequence

https://www.crocus.co.kr/681

https://www.acmicpc.net/workbook/view/801

https://jaimemin.tistory.com/1095

https://www.geeksforgeeks.org/longest-monotonically-increasing-subsequence-size-n-log-n/

https://jason9319.tistory.com/113

https://medium.com/@sasaxxx777/cors-misconfiguration-leading-to-private-information-disclosure-9ebcbd4740d9

 

CORS Misconfiguration leading to Private Information Disclosure

Hi hackers, today i will talk about CORS that i found in a private program

medium.com

본 포스팅은 위 블로그 글의 번역본입니다.

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

 

이번 글에서는 CORS에 관하여 다루어볼 예정이다.

 

일단 이 사이트를 private.com이라고 해보자.

 

이것 저것 좀 시도해본 뒤, 이 사이트의 서브도메인을 찾았는데 이거를 xyz.private.com이라고 부르겠다.

 

그리고 나서 burp suite로 크롤링이랑 파라메터 digging을 시도했고, origin 헤더랑 같이 요청이 날아갔다.

origin 헤더는 이 요청이 어느 도메인에서 시작되었는지를 나타내는 HTTP 헤더이고, path information은 갖지 않는다. 즉 xyz.private.com/index.php 에서 요청을 보냈다면 /index.php 부분의 값은 무시하고 domian 부분만 해당한다는 뜻.

 

Origin 헤더는 Access-Control-Allow-Origin 헤더에 의해 accept되었고, Access-Control-Allow-Credentials 헤더가 true로 값이 설정되었다.

 

Access-Control-Allow-Origin 헤더는, 해당 Origin에서 온 요청이 Accept될 것인지 아닌지를 알려주는데, xyz.private.com이 값으로 있으므로, 해당 URL의 Origin에서 시작된 요청은 Accept된다는 뜻이다. Origin 구분없이 Accept될 것이라면 *라고 쓰여있을 것이다.

 

Access-Control-Allow-Credentials라는 HTTP 헤더는 응답 헤더인데, credential이 포함된 요청을 보냈을 때, 응답 값을 브라우저가 프론트엔드 자바스크립트한테 요청할 지 안할지를 정해주는 그런 헤더라고 한다.

 

이 현상을 보고, 저자는 서버가 어떻게 origin header를 처리하는지를 조사를 했다.

그리고 원래 메인 도메인으로 요청을 보냈고, 서버는 이에 대하여 accept 했다.

그래서 private.com에서 점을 지우고 보냈는데, 서버가 또다시 accpet을 해 주었다. 서버는 도메인 이름과 com이 있는지만 체크하고, 그 사이에는 무엇이 있든간에 무시하는 것 같았다. 그래서 private와 com사이에 1을 넣어서 보내보았다.

그리고 똑같이 accept가 응답이 돌아왔다.

 

그래서 privatescom.000webhostapp.com이라는 주소로 하나 도메인을 만들었고, 이를 이용해서 POC 코드를 작성해 보았다.

 

그리고 만든 도메인에서 응답을 받을 수 있었고, 유저 정보를 훔칠 수 있었다.

'해킹 & 보안' 카테고리의 다른 글

wechall.net(위첼) 소개  (0) 2020.06.30
Gopher protocol 간단 분석  (0) 2020.06.20
워게임 사이트 root-me.org  (0) 2019.11.27
checksec.sh Linux ver  (0) 2019.07.25
Caesar Cipher Text Online Codec (Batch)  (0) 2019.07.25

바이너리 서치(binary search)

바이너리 서치는 아마 컴퓨터공학과 1~2학년이라면 자료구조시간이나 알고리즘 시간에 이름 한번쯤은 들어봤을 것이다.

그만큼 기본적이지만 강력한 알고리즘 중 하나이다. 

근데 요녀석은 정렬처럼 그냥 라이브러리 하나 호출해서 띡~ 하고 끝날때도 있는데, 뭔가 응용해야할 때가 간혹 있다.

 

대충 한번 정리해보자.

 

일단 바이너리 서치를 할려면 선행조건이 있다. 

정렬되어 있어야 한다는 점

 

그래서 중간에 하나 아무거나 짚어서 봐도 그 이전과 그 다음 값들이 어떨지 예상을 할 수 있는 것이다. 이를 이용해서 시간복잡도를 //(O(lgN)//)으로 줄일 수 있다.

 

일단그리고 C++ SLT에 binary_search라는 함수가 있다. 배열같은 컨테이너 시작과 끝 iterator를 넣고, 찾는 값을 넣으면 bool을 리턴해준다. 원하는 값이 있으면 true, 그렇지 않으면 false를 리턴한다.

 

근데 그러면 내가 찾는 녀석이 어디쯤에 존재하는지를 알 수가 없다.

그래서 보통 직접 구현해서 쓰곤 한다.

 

lower_bound와 upper_bound

근데 생각을 해 보자. 원래 배열이 잘 정렬되어 있긴 한데, 중복된 수가 있다면?

그리고 그 중복된 수가 몇 개인지를 알아야 하는 경우라면? 아니면, 그 중복된 수 모두를 체크해야 하는 경우라면 어떻게 해야할까?

 

위와 같이 중복이 있는 배열이라고 할 때, 모든 40을 다 찾아야 한다면 다음과 같이 lower bound와 upper bound를 이용하면 된다.

lower_bound(40)을 행하면 첫번째로 나타나는 40의 인덱스인 4를 리턴하고, upper_bound(40)을 행하면 마지막으로 나타나는 40의 인덱스 다음 값인 7을 리턴한다.

 

그러면 lower_bound부터 upper_bound까지 for-loop을 돌면 모든 40인 값들을 찾을 수 있다.

 

그렇다면 lower_bound(35)를 수행한다면? 35는 배열에 없는 값이다.

그러면 35가 들어갈 수 있는 4라는 인덱스를 똑같이 리턴한다. 4번자리는 기존 40이 있던 자리이다. 그러면 4번 자리에 35를 넣고, 기존 값들인 40, 50 등 을 한 칸씩 뒤로 밀면 35가 들어가도 전체 배열은 그대로 정렬된 상태를 유지하게 된다.

 

마찬가지로 그렇다면 upper_bound(35)를 수행한다면? 35는 배열에 없는 값인데, lower_bound(35)를 한 값과 마찬가지로 4라는 인덱스를 리턴한다. 마찬가지로 4번 자리에 35를 넣을 수 있다는 뜻이다.

 

배열에 없는 값에 대하여 lower_bound나 upper_bound 연산을 수행하면 같은 값이 나오게 된다.

이는 즉, 배열 안에 있는 x값의 개수를 찾고 싶다면 upper_bound(x) - lower_bound(x)라는 연산으로 할 수 있다.

 

그렇다면 그 정의는 다음과 같게된다.

lower_bound

- x라는 값을 배열에 넣을 때, 정렬된 상태를 유지하면서 넣을 수 있는 위치 중 가장

(이때 넣는다는 개념은 해당 자리에 x를 넣고, 기존 값들은 다 1칸씩 뒤로 민다고 가정함)

- x라는 값 보다 같거나 큰 값가장 앞에 있는 원소 위치

 

upper_bound

- x라는 값을 배열에 넣을 때, 정렬된 상태를 유지하면서 넣을 수 있는 위치 중 가장

- x라는 값보다 큰 값가장 앞에 있는 원소 위치

 

 

 

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

https://12bme.tistory.com/120

http://soen.kr/lecture/ccpp/cpp4/42-3-2.htm

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

 

12877번: 먹이 사슬

BOJ 행성에는 N마리의 동물들이 살고 있습니다. 민호는 이 동물들을 구분하기 위해 1, 2, ..., N의 번호를 붙였습니다. 또한 BOJ 행성에 살고 있는 모든 동물들은 A, B, C의 세 종류 중 하나입니다. 민호는 재미있는 점을 발견 했는데 A는 B를 먹고 B는 C를 먹고 C는 A를 먹는다는 사실 입니다. 오랜 기간 행성을 관찰한 민호는 자신이 기록한 내용을 기반으로 BOJ 행성의 생태 지도를 그려보려 합니다. 민호가 기록한 내용은 아래 두 종류

www.acmicpc.net

설날 큰집을 가면서 노란책을 들고갔는데, 책을 보다가 나온 POJ문제 풀이를 보다가 혹시 백준에도 같은 문제가 있나 싶어서 확인해보니 있었다.

 

그래서 백준에 있는 김에 한번 풀어보았다.

 

POJ에서는 1182번 문제였는데, 직접 가보니까 중국어로 문제가 써있어서 해석은 안된다 싶었는데, Sample Input 까지도 일치하더라. 대신 크롬에 내장된 구글 번역기를 사용하니 그런대로 해석은 되는 듯 하다.

 

 

 

문제 분석

일단 나는 책을 보면서 문제를 봤기 때문에, 풀이를 거의 바로 보아서 힌트 없이 스스로 생각해볼 만한 시간이 조금 부족하긴 했다.

 

대충 보면 명제와 관련된 문제이고, 논리와도 관련이 있다. 여태까지의 정보를 조합해서 모순이 되는 논리를 찾아야 한다.

 

내가 풀고나서 찍은 스샷이니, 내가 풀 당시에는 15명밖에 안푼 문제이기도 하다.

 

일단 풀이부분은 스포일러 방지로 접은글로 표현하겠다.

 

풀이

더보기

논리식을 만들어서, 같이 성립될 수 있는 논리들을 묶는 방식으로 풀이를 진행한다. Union-Find라고 불리는 Disjoint set 자료구조를 이용해서 묶으면 된다.

 

동물의 번호를 0 ~ N-1까지 매긴다고 하면, 3N개의 논리를 만든다.

각각 i번 동물이 A 종류이다 / B종류이다 / C종류이다. 라는 논리들이다.

 

만약 N = 3이라고 한다면

0번 - 0번 동물이 A종류이다.

1번 - 1번 동물이 A종류이다.

2번 - 2번 동물이 A종류이다.

 

3번 - 3번 동물이 B종류이다.

4번 - 4번 동물이 B종류이다.

5번 - 5번 동물이 B종류이다.

 

6번 - 6번 동물이 C종류이다.

7번 - 7번 동물이 C종류이다.

8번 - 8번 동물이 C종류이다.

 

요런식으로 구분이 되게 된다.

그러면 주어진 정보에 따라서 적절한 정보들을 Union 연산을 하는데, 만약 모순되는 내용이 같은 집합에 들어가게 된다면 무시하고 답 수를 1만큼 늘려야 한다.

 

x번 동물과 y번 동물이 같은 종류이다 라는 명제가 들어왔을때, 이미 두 동물이 다른 종류이다 라는 명제가 동시에 성립한다면(같은 집합 안에 있다면) 이번 명제는 모순이 되는 것이다.

다른 동물일 수 있는 경우의 수는 전체 경우의 수 3x3 = 9가지 중 6가지가 해당되게 된다.

 

x번 동물이 y동물을 잡아먹는다고 하면, 마찬가지로 6가지 모순인 경우의 명제가 이미 같이 성립하고 있다면 이번 명제가 모순이 되게 된다.

 

 

코드

더보기
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\n'
#define MAXN 50020

// disjoint set (DSU)
int parent[MAXN * 3];
int depth[MAXN * 3];

int root(int a) {
	int p = a;
	while (p != parent[p])
		p = parent[p];
	parent[a] = p;
	return p;
}
bool same(int a, int b) {
	return root(a) == root(b);
}
void unite(int a, int b) {
	a = root(a);
	b = root(b);
	if (a == b) return;
	if (depth[a] < depth[b]) {
		parent[a] = b;
	}
	else {
		parent[b] = a;
		if (depth[a] == depth[b])
			depth[b]++;
	}
}
int main() {
	int N, K;
	scanf("%d%d", &N, &K);
	//init
	for (int i = 0; i <= N * 3 + 10; i++) {
		parent[i] = i;
		depth[i] = 0;
	}
	// i => i는 A이다.
	// i + N => i는 B이다.
	// i + N*2 => i는 C이다.
	int ans = 0;
	while (K--) {
		int t, a, b;
		scanf("%d%d%d", &t, &a, &b);
		a--;
		b--;
		if (a < 0 || b < 0 || a >= N || b >= N) {
			ans++;
			continue;
		}
		if (t == 1) {
			//a랑 b랑 같은 종류이다.
			if (same(a, b + N) || same(b, a+N) || same(a+N, b+N+N) || same(b+N, a+N+N) ||
				same(a, b + N + N) || same(b, a + N + N)) {
				ans++;
			}
			else {
				unite(a, b);
				unite(a + N, b + N);
				unite(a + N + N, b + N + N);
			}
		}
		else {
			//a는 b를 잡아먹는다.
			if (same(a, b) || same(a + N, b + N) || same(a + N + N, b + N + N) ||
				same(a, b + N + N) || same(a + N, b) || same(a + N + N, b + N)) {
				ans++;
			}
			else {
				unite(a, b + N);
				unite(a + N, b + N + N);
				unite(a + N + N, b);
			}
		}
	}
	printf("%d\n", ans);
	return 0;
}

노란책의 솔루션 코드를 보면, 조건을 6개를 다 명시하지 않아도 정답이 된다고 하는데 왜 그런지는 아직 이해가 안된다;;

if (t == 1) {
	//a랑 b랑 같은 종류이다.
	//if (same(a, b + N) || same(b, a + N) || same(a + N, b + N + N) || same(b + N, a + N + N) ||	same(a, b + N + N) || same(b, a + N + N)) {
	if (same(a, b+N) || same(a, b+N+N)) {
		//다른걸로 분류하면 모순
		ans++;
	}
	else {
		unite(a, b);
		unite(a + N, b + N);
		unite(a + N + N, b + N + N);
	}
}
else {
	//a는 b를 잡아먹는다.
	//if (same(a, b) || same(a + N, b + N) || same(a + N + N, b + N + N) || same(a, b + N + N) || same(a + N, b) || same(a + N + N, b + N)) {
	if (same(a,b) || same(a, b+N+N)) {
		ans++;
	}
	else {
		unite(a, b + N);
		unite(a + N, b + N + N);
		unite(a + N + N, b);
	}
}

이 코드로도 정답이 되는데 왜 그런지는 아직 이해가 안간다. 천천히 이해해봐야할듯 하다.

 

이 글은 제 개인적인 의견을 담고 있는 글입니다.

 

SW취업시장에서 필수가 된 알고리즘 문제해결능력(?)

요즘은 여러모로 경기가 좋지 않아서 왠만한 명문대 졸업자들도 취직을 하는게 하늘에 별따기 같습니다.

4차산업혁명 이야기를 막 하면서도, SW계열로 직업을 구하고자 하면, 이것 저것 능력을 많이 따져서 신입사원으로 지원하는 것임에도 불구하고 지원자에게 요구하는 능력 수준이 생각보다 꽤나 높습니다.

 

이름만 들으면 알만한 IT회사나 대기업들의 경우 신입사원 공채나 상시채용 프로세스에서 알고리즘 문제해결능력을 확인하는 과정들이 이제는 거의 필수처럼 되어가고 있는데, 왜 그런걸까요?

 

왜 IT SW계열 회사에서 알고리즘 능력을 강조할까?

Problem Solving(PS) vs Competitive Programming(CP)

일단 간단하게 용어를 조금 정하고 넘어가자면, 알고리즘 문제해결 분야를 보통 PS라고 줄여서 말합니다. Problem Solving의 약자로, 문제해결이라는 뜻이지요. 소프트웨어쪽에서 PS라는 단어를 쓰면 보통 이 용어를 뜻하는 것입니다.

 

비슷한 단어로 CP라는 단어가 있는데 이는 Competitive Programming의 약자로, 쉽게 말해서 코딩대회 같은 것을 생각하시면 됩니다. PS와 CP는 둘다 코딩할 수 있는 알고리즘 문제를 푼다는 공통점이 있는데, 조금 차이점이라고 하면 CP(Competitive Programming)의 경우 대회에서 경쟁하는 방식이므로 보통 수 시간(2~3시간)동안 문제를 빠르고 정확하게 풀면서 서로 점수와 푼 문제수로 경쟁한다고 한다면, PS(Problem Solving)는 문제를 푸는 것 그 자체에 포커스가 맞추어져 있습니다.

 

즉 대회에서 문제를 푸는게 아니라, 하나의 문제를 잡고 일주일이건 몇달이건 계속 풀고, 더 나은 해법을 찾고 하는 그런 행위들을 PS라고 하는 것이지요. PS를 평소에 즐겨해서 훈련이 되어 있다면 CP에도 도움이 될 것입니다. 다만 CP에서 좋은 성적을 얻기 위해서는 문제를 빨리 풀고 구현하는 능력도 훈련이 되어있어야겠지요?

 

IT회사에서 채용할때 PS능력을 얼마나 보는데?

일단, 국내 IT회사라고 하면 딱 떠오르는 네이버, 카카오, 라인플러스, NHN 등등이 있는데 이 회사들은 채용시 다 코딩테스트를 봅니다.

네이버의 경우는 공채를 자주 하지는 않지만, 최근에 한번 했엇지요? 그때도 서류(자기소개서) 제출 이후  온라인 코딩테스트를 통해서 지원자를 걸렀습니다.

라인플러스와 NHN도 신입사원 공채시 코딩테스트가 들어있었고, 일정 이상 점수를 받지 못한 사람들은 면접으로 가지 못했습니다.

카카오의 경우는 2017년 부터 매년 블라인드 코딩테스트라는 제도를 만들어서, 서류심사 없이 1차 2차 코딩테스트의 결과만으로 지원자들을 면접전형까지 보게 해주는 식으로 채용을 진행하였습니다. 그만큼 PS능력을 매우 중요시 한다는 것이지요. 수 많은 스펙보다 코딩 잘하는게 더 잘 증명되어있다 라고 생각하는 그런 부분이 보이는 현상이라고 생각합니다.

 

이 외에도 대부분의 IT회사들은 코딩테스트를 보거나, 상시채용을 실시하는 경우는 대규모로 같이 보는 것이 아니므로, 면접에서 이러한 알고리즘 문제해결능력을 알아볼 수 있는 질문들을 하곤 합니다.

이러다 보니, 코딩테스트 환경을 대행해주는 서비스 영역도 생기게 되었지요.

 

대기업의 경우에는 공채 프로세스에서 대부분 인적성이라고 하는 NCS와 비슷한 필기 문제를 통해서 사람들을 거르게 되는데, 삼성전자의 경우는 소프트웨어 직군의 엔지니어를 채용할 때에는 삼성계열사 인적성인 GSAT대신 SW역량테스트라는 알고리즘 문제를 냅니다.

그리고 채용 이후에도 신입사원 교육때 알고리즘과 같은 기초 부분은 중점적으로 다시 교육시킵니다.

 

그리고 세계적인 IT회사인 구글의 경우도 알고리즘 문제해결능력을 매우 Intensive하게 체크한다는 것도 널리 알려져 있습니다.

 

형태는 다들 다르긴 하지만, SW엔지니어를 채용할 때 알고리즘 문제해결 능력을 중요시 하다는 점은 자명해졌습니다.

 

그러면 PS를 잘하면 뭐가 좋을까요?

 

알고리즘 문제 해결(PS)능력이 뛰어나면 잘하는 것들

하지만 사실 알고리즘 문제 해결능력이 뛰어나면 부수적으로 딸려오는 능력들이 있습니다.

구현 능력

어떤 비즈니스 로직이나 알고리즘을 구현하는 능력이 꽤 많이 능숙해집니다. 빠르고 정확하게 구현할 수 있는 능력이 생기는데, 이는 SW엔지니어에게는 기초체력과 같은 능력이지요. 그리고 이런 능력은 은근히 기르기에 어렵고 시간도 많이 걸리는데, PS와 CP를 즐겨하는 사람들에게는 기본적으로 같이 있는 능력입니다.

 

시간복잡도와 공간복잡도에 대한 이해

물론 이 복잡도들은 학부 1~2학년때 배우는 내용이므로, 기초적인 내용이긴 하지만 실제로 구현을 하고 잘못 짜서 TLE(Time Limit Exceed) 판정을 보면서 문제를 풀어온 사람들에게는 매우 직관적으로 잘 이해되게 됩니다.

유명한 알고리즘과 자료구조들에 대한 이해

마찬가지로 자주쓰이는 balanced tree나 hash table과 같은 자료구조들, 그리고 유명한 알고리즘 dfs, bfs, dijkstra's shortest path 등에 대한 이해도가 남다릅니다. 직접 구현까지 해보고 시간복잡도 공간복잡도도 다 분석해본 사람들이니 말입니다.

남의 코드를 읽는 능력

PS를 하다보면, 남이 작성한 정답코드를 확인해 볼 일이 꽤 있는데, 이 과정에서 남이 작성한 코드를 이해하는 능력이 많이 길러지게 됩니다. 이 능력 역시 매우 기르기 어려운 능력인데, 자연스럽게 길러지는 것이지요.

 

알고리즘 문제를 잘 풀면 실무(개발)를 잘하나?

실무 != 알고리즘

사실 어플리케이션단의 개발을 하려면 알고리즘 문제를 매우 Intensive하게 잘 할 필요는 없습니다. 그리고 보통 알고리즘 문제해결능력이 좋으면 다 장땡이다 라는 생각을 비판하는 분들도, 알고리즘 잘 풀어도 일 잘하는거랑은 관계없다, 알고리즘 못해도 일 잘하는 사람 있다 라는 의견들을 많이들 내십니다.

 

물론 맞는말입니다. 요즘은 라이브러리나 오픈소스들이 잘 되어 있어서 그러한 Low-level 부분들을 하나하나 다 구현하지 않아도 잘 사용할 수 있습니다. //(O(NlgN)//)의 정렬을 구현하지 못해도, Java나 C등의 기본 라이브러리에 있는 sort라는 함수를 한번만 호출해주면 알아서 정렬을 해 주고, HashMap같은 것을 직접 구현하지 않아도, 라이브러리에 있는 녀석들을 사용하면 간단하게 개발을 할 수 있습니다.

 

그리고 요즘 핫한 딥 러닝이나 블록체인 개발/구현 등을 한다고 할 때, 이러한 알고리즘 문제 풀이 능력은 크게 도움이 안되는 것 처럼 보입니다. 알고리즘 문제 풀 시간에, 최신 딥 러닝 논문을 한 편 더 보는게 도움이 더 될 것이다, 이런 생각들이 들겠지요

 

알고리즘 < 실무

실무라고 하는 범위가 매우 다양할 수 있기 때문에, PS능력이 절대적으로 모든 실무능력을 다 커버해주진 않습니다. 하지만 실무를 하면서 맞닥드릴 수 있는 상황들을 쉽게 해쳐나갈 수 있는 능력을 줍니다.

PS에 능숙한 사람이라면 다음과 같은 상황은 생각보다 쉽게 해쳐나갈 수 있습니다.

 

1. 내가 원하는 기능을 정확히 수행하는 라이브러리나 오픈소스가 없을 경우 직접 구현하거나, 기존 것을 변경해서 사용해야하는 경우

2. 남이 작성한 코드를 빠르게 이해해야 하는 경우

3. 직접 구현할 때 실수 없이 빠르게 구현해야 하는 경우

 

특히나 어플리케이션 단이 아닌 임베디드와 같이 물리적 제약조건이 많은 환경이나, OS/커널과 같이 Low-level에서 구현해야 하는경우, 그리고 코어한 엔진부분 구현이라던지 하는 부분은 직접 작성해야하는 경우가 많습니다.

 

라이브러리에 잦은 의존을 하던 사람은 이러한 코어개발자가 될 기회를 PS를 즐겨하는 사람에게 뺏길 가능성이 높습니다.

 

언젠가는 주니어 SW엔지니어 중에서도 대부분이 어려워하는 분야도 해낼 수 있는 시니어 엔지니어로 거듭나야하는데, 이 과정에서 PS역량이 많은 도움이 된다고 볼 수 있습니다.

 

계속 변하는 기술트랜드

요즘은 기술을 배우는 시간보다 새로운 기술이 나타나는 시간이 더 빠르다는 이야기도 있습니다. PS능력은 새로운 기술이 나왔을 때 빠르게 배울 수 있는 기초체력과 같은 능력이 됩니다. 

사실 요즘 핫한 딥 러닝이 유행을 탄 지도 5년 정도도 채 안되었다고 볼 수 있습니다. 만약에 곧 딥러닝의 인기가 시들해지고, 새로운 기술이 각광받는다면 또 그 기술을 배워야 하는데, 어차피 이렇게 계속 기술을 배워야 한다면, 몇년만에 유행을 타는 기술들이 아닌, 한번 제대로 배워놓으면 수십년은 가는 컴퓨터공학의 근간을 이루는 시스템, 네트워크, 자료구조, 알고리즘 등의 실력을 탄탄히 길러놓는 것이 현명한 방법 아닐까요?

 

끝으로 구사과님이 알고리즘 대회 열심히하는게 무용하다'라는 의견의 코드포스 블로그에 남긴 댓글 링크를 걸면서 이만 글을 줄이겠습니다.

 

 

 

http://codeforces.com/blog/entry/49289#comment-332844

 

추가적으로 더 읽을만한 글도 있습니다.

https://www.acmicpc.net/blog/view/49

CP와 PS를 비교하면서, CP를 하면서 점수(레이팅)에 집착하지 말자는 그러한 내용의 글입니다.

 

https://medium.com/@ghilbut/%EC%8B%A4%EB%AC%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%97%90%EA%B2%8C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%80-%EB%8D%9C-%EC%A4%91%EC%9A%94%ED%95%A0%EA%B9%8C-2-45714fc83e15

English write-up

http://lowdeep.insomnihack.ch/

 

Insomni'hack 2020 Teaser

LowDeep Plateform That's our new plateform to perform ping the easy way.

lowdeep.insomnihack.ch

Reading the text, this is a kind of ping service.

 

Let's input the 127.0.0.1

Output of input "127.0.0.1" is command line stdout of linux ping command.

 

We can easily guess this it the bash shell command injection challenge.

 

I did some trials about well-known command injection symbols like ; | & $

 

Only ; character is not filtered by the server.

 

Let's input the 127.0.0.1;ls

 

 

Any other command like cat, /bin, bash is not applicable for that server. I tried "ls -alR" also.

I searched for some files like 'robot.txt', and there are some texts but it was not something important.

 

When I tried to connect the server with path /print-flag, I can get 64bit ELF binary file.

Just executing the binary, we can find somthing.

$ file print-flag
print-flag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=72c589834f878a6a3267944f305c29166a1ace8b, stripped
$ ./print-flag
INS{Wh1le_ld_k1nd_0f_forg0t_ab0ut_th3_x_fl4g}

We've got the flag.

한글 풀이

http://lowdeep.insomnihack.ch/

 

Insomni'hack 2020 Teaser

LowDeep Plateform That's our new plateform to perform ping the easy way.

lowdeep.insomnihack.ch

글을 읽어보니, ping 서비스를 해주는 페이지라고 한다.

 

127.0.0.1를 한번 넣어보자.

결과는 리눅스 커맨드라인에서 ping 127.0.0.1한 결과가 나온다.

 

간단하게 커맨드 인젝션 문제임을 추측할 수 있다.

 

몇가지 커맨드 인젝션 관련된 심볼들을 넣어봤는데, (; | & $ 같은녀석들)

 

; 문자만 통하는 것 처럼 보였다. 나머지는 다 Forbidden이 뜬다.

 

127.0.0.1;ls를 입력해보자.

 

다른 cat이나 /bin/bash 같은 문자들도 다 필터링 되거나 permission denied가 뜨거나 했다. 그래서 ls -alR로 디렉토리 계층구조를 확인해보았다.

 

몇가지 특이해보이는 파일들이 있는데, robots.txt라던지 등이 있다. 하지만 실제로 들여다 보면 뭔가가 있기는 한데 플래그를 얻는데 필요하거나 중요한 내용들은 아니었다.

 

URL경로에 /print-flag 로 접근하니 64비트 ELF 바이너리 파일을 얻을 수 있었다.

 

실행을 시켜보니 플래그가 나왔다.

$ file print-flag
print-flag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=72c589834f878a6a3267944f305c29166a1ace8b, stripped
$ ./print-flag
INS{Wh1le_ld_k1nd_0f_forg0t_ab0ut_th3_x_fl4g}

 

Microsoft SQL Server와 SSMS를 설치했으니 이를 이용해서 쿼리를 한번 써 봐야겟다.

SQL 쿼리문은 SQL 표준 쿼리를 대부분 지원할 것이고, MS-SQL 만의 특별 기능들이 조금 있을 수 있다. 이번 포스팅에서는 그러한 기본 SQL 쿼리문은 다 안다고 생각하고, SSMS의 GUI기능들을 최대한 활용하는 방식으로 접근해보고자 한다.

 

SSMS 실행 및 DB연결

일단 SSMS를 실행해야 하므로, 시작메뉴에서 SSMS를 검색한다.

Microsoft SQL Server Management Studio라는게 뜬다. 클릭해서 실행한다.

 

프로그램이 꽤나 무거워서 실행하는데 시간이 좀 걸린다.

 

SQL 서버 접속 관련된 내용이 뜨는데, 그냥 기본값 그대로 연결을 눌러본다.

 

새 데이터베이스 생성

그리고 새 데이터 베이스를 생성하기 위해 좌측 트레이에서 데이터베이스 항목에 우클릭을 한다.

데이터베이스 이름에 test라고 입력 해보았다. 그러자 아래에 논리적 이름에 2개의 항목이 자동으로 이름이 기입이 된다.

 

그리고 확인 버튼을 눌러서 새 데이터베이스를 추가한다.

새 데이터베이스 "test"가 추가된 것을 알 수 있다.

GUI 방식 말고도, SQL쿼리로 데이터베이스가 어떻게 생겨났는지를 확인할 수 있는 방법이다. 위와 같은 경로로 따라가서 클릭하게 되면 아래와 같은 트레이가 나타나게 된다.

해당 db를 만들 때 위와 같은 SQL 쿼리로 만들어졌음을 알 수 있다.

 

테이블 생성

테이블을 생성할 데이터 베이스의 하위 기능을 확장한 뒤, 테이블 쪽에서 마우스 우클릭을 해서 새로 만들기 - 테이블을 클릭한다.

그러면 위와 같이 테이블 생성 마법사가 나타나게 된다.

열 이름과, 데이터 형식을 설정한 뒤 탭에 마우스 우클릭을 해서 저장을 누른다.

테이블 명을 입력하라고 하는데 user라고 입력했다.

그리고 나서 좌측 창에 있는 새로고침 버튼을 눌러본다.

그러면 하단에 새로운 테이블 dbo.user가 추가된 것을 확인할 수 있다.

 

테이블에 데이터 추가

이제 user라는 테이블에 데이터를 추가해보자.

위와 같은 경로로 새 쿼리 편집기 창을 켜면, INSERT INTO 쿼리가 어느정도 완성되어서 나타나게 된다.

values에 값을 넣어보도록 하자.

그리고 쿼리를 실행하기 위해 실행 버튼을 누르도록 하자.

값을 넣고 잘 들어갔는지 확인을 해보자.

그러면 결과값을 볼 수 있다.

값이 잘 들어간 것을 확인할 수 있다.

 

 

 

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

https://www.guru99.com/ms-sql-server-tutorial.html
https://docs.microsoft.com/ko-kr/sql/relational-databases/database-engine-tutorials?view=sql-server-ver15
https://www.guru99.com/sql-server-database-create-alter-drop-restore.html

+ Recent posts