이분탐색

 

정렬되어 있는 배열에서 특정 데이터를 찾기 위해 모든 데이터를 순차적으로 확인하는 대신, 탐색 범위를 절반으로 줄여가며 찾는 탐색 방식.

 

이분탐색의 주의 사항

 

  1. 주어진 배열은 반드시 오름차순으로 정렬되어 있어야 한다.
  2. 무한 루프에 빠지지 않게 mid 값을 정해야 한다.

 

정렬된 배열에서 중복된 데이터를 제거할 땐 unique와 erase를 이용한다.

2개의 값들을 묶거나 어느 한쪽의 값을 이분탐색으로 찾아서 시간복잡도를 낮추는 아이디어는 이분탐색 관련 응용문제에서 핵심적으로 많이 나온다.

 

Parametric Search (매개 변수 탐색)

 

조건을 만족하는 최소/최댓값을 구하는 문제(최적화 문제)를 결정 문제로 변환해 이분탐색을 수행하는 방법

최적화 문제를 결정 문제로 바꿀 수 있는지 생각하고 그 결정 문제로 얻어낸 함수가 감소 혹은 증가함수인지를 따져봐야 한다.

 

문제에서 최소/최대 얘기가 있고 범위가 무지막지하게 크거나 시간 복잡도에서 값 하나를 로그로 떨어트리면 될 것 같을때 Parametric Search 풀이를 고민할 수 있다.

 

 

연습 문제

 

1920: 수 찾기

더보기
#include <iostream>
#include <algorithm>

int arr[100001];
int n, m;

int bsearch(int &t)
{
	int st = 0;
	int en = n - 1;

	while (st <= en) {
		int mid = (st + en) / 2;

		if (arr[mid] > t) en = mid - 1;
		else if (arr[mid] < t) st = mid + 1;
		else return 1;
	}
	return 0;
}

using namespace std;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	for (int i = 0; i < n; ++i)
	{
		cin >> arr[i];
	}
	sort(arr, arr + n);
	cin >> m;
	while (m--)
	{
		int t;
		cin >> t;
		cout << bsearch(t) << '\n';
	}

	return 0;
}

 

10816: 숫자 카드 2

더보기
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main(void)
{
	ios_base::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);

	vector<int> cards;	
	int n, m, data;

	cin >> n;

	while (n--)
	{
		cin >> data;
		cards.push_back(data);
	}

	sort(cards.begin(), cards.end());

	cin >> m;

	while (m--)
	{
		cin >> data;
		cout << upper_bound(cards.begin(), cards.end(), data) - lower_bound(cards.begin(), cards.end(), data) << " ";
	}

	return 0;
}

 

lower_bound와 upper_bound의 결과를 pair로 반환해주는 equal_range라는 함수가 있다.

이 경우 second - first로 더 간단하게 표현할 수 있다.

 

18870: 좌표 압축

더보기
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int arr[1000001];
vector<int> ans;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	cin >> n;
	for (int i = 0; i < n; ++i)
	{
		cin >> arr[i];
		ans.push_back(arr[i]);
	}
	sort(ans.begin(), ans.end());
	ans.erase(unique(ans.begin(), ans.end()), ans.end());

	for (int i = 0; i < n; ++i)
	{
		cout << lower_bound(ans.begin(), ans.end(), arr[i]) - ans.begin() << ' ';
	}

	return 0;
}

 

2295: 세 수의 합

더보기

추후 풀어볼 예정

 

1654: 랜선 자르기

더보기
#include <iostream>
#include <algorithm>

using namespace std;
using ll = long long;

int k, n;
int arr[1000001];

// 핵심 메소드
bool cut(int v)
{
	ll ans = 0;
	for (int i = 0; i < k; ++i)
		ans += arr[i] / v;
	return ans >= n;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> k >> n;
	for (int i = 0; i < k; ++i)
		cin >> arr[i];

	sort(arr, arr + k);

	ll st = 0;
	ll en = (1<<31)-1;
	while (st < en)
	{
		ll mid = (st + en + 1) / 2;
		if (cut(mid)) st = mid;
		else en = mid - 1;
	}
	cout << st;

	return 0;
}

 

Parametric Search의 대표 문제.

최적화 문제 : N개를 만들 수 있는 랜선의 최대 길이

->결정 문제 : 랜선의 길이가 X일 떄 랜선이 N개 이상인가 아닌가?

 

기본 문제

 

10815: 숫자 카드

더보기
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int n, m;
int v1[500001];
int v2[500002];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	for (int i = 0; i < n; ++i)
		cin >> v1[i];

	cin >> m;
	for (int i = 0; i < m; ++i)
		cin >> v2[i];

	sort(v1, v1 + n);

	for (int i = 0; i < m; ++i)
		cout << binary_search(v1, v1 + n, v2[i]) << ' ';

	return 0;
}

 

1822: 차집합

더보기
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;
using ll = long long;

int n, m;
ll arr1[500001];
ll arr2[500001];
vector<ll> answer;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n >> m;

	for (int i = 0; i < n; ++i)
		cin >> arr1[i];
	for (int i = 0; i < m; ++i)
		cin >> arr2[i];

	sort(arr1, arr1 + n);
	sort(arr2, arr2 + m);

	for (int i = 0; i < n; ++i)
		if (!binary_search(arr2, arr2 + m, arr1[i])) answer.push_back(arr1[i]);

	cout << answer.size() << '\n';
	for (auto& elem : answer)
		cout << elem << ' ';

	return 0;
}

 

16401: 과자 나눠주기

더보기
#include <iostream>
#include <algorithm>

using namespace std;
using ll = long long;

int n, m;
int arr[1000001];

// 이 함수를 잘 기억해야 한다
bool cut(int v)
{
	if (v == 0) return true;
	ll ans = 0;
	for (int i = 0; i < m; ++i)
		ans += arr[i] / v;
	return ans >= n;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n >> m;
	for (int i = 0; i < m; ++i)
		cin >> arr[i];

	sort(arr, arr + m);

	int st = 0;
	int en = 1000000000;
	while (st < en)
	{
		int mid = (st + en + 1) / 2;
		if (cut(mid)) st = mid;
		else en = mid - 1;
	}
	cout << st;

	return 0;
}

 

Parametric Search

최적해가 없는 경우에 0을 출력해야 하므로 cut 메소드에서 0일때도 true를 반환하도록 한다.

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x15] 해시  (0) 2022.08.06
[0x14] 투 포인터  (0) 2022.08.05
[0x12] 수학  (0) 2022.08.05
[0x11] 그리디  (0) 2022.08.04
[0x10] 다이나믹 프로그래밍  (0) 2022.08.03

소수

 

소수 판정법

 

합성수 N에서 1을 제외한 가장 작은 약수는 √N 이하이다.

2부터 √N까지의 수로 나누어지지 않으면 소수이다.

sqrt는 실수값으로 반환되므로 오차가 생길수 있어서 사용하지 않는게 좋기때문에 i의 루프 범위를 i*i <= n으로 잡아주면 된다.

 

범위 내에서의 소수 판정법 - 에라토스테네스의 체

더보기
vector<int> sieve(int n) {
	vector<int> primes;
	vector<bool> state(n + 1, true);
	state[1] = false; // 없어도 됨
	for (int i = 2; i <= n; ++i) {
		if (!state[i]) continue;
		for (int j = 2*i; j <= n; j += i)
			state[j] = false;
	}
	for (int i = 2; i <= n; ++i)
		if (state[i]) primes.push_back(i);
	return primes;
}

 

시간복잡도 개선안

vector<int> sieve(int n) {
	vector<int> primes;
	vector<bool> state(n + 1, true);
	state[1] = false; // 없어도 됨
	for (int i = 2; i*i <= n; ++i) {
		if (!state[i]) continue;
		for (int j = i*i; j <= n; j += i)
			state[j] = false;
	}
	for (int i = 2; i <= n; ++i)
		if (state[i]) primes.push_back(i);
	return primes;
}

 

시간복잡도는 O(NloglogN) 이다.

짤막한 팁) bool type을 vector로 사용하면 1바이트가 아닌 1비트로 저장되어 공간을 줄일 수 있다.

 

소인수 분해

 

  1. N을 i로 나눌수 있으면 i로 나누어지지 않을 때 까지 i를 소인수 목록에 추가하고 N을 i로 나눈다(i >= 2)
  2. i로 나누어지지 않으면 i를 증가시킨다.
  3. N이 1이 될 때까지 1~2를 반복한다.

 

소인수 목록에 들어간 수는 반드시 소수이다.

위와 같은 방식의 시간복잡도는 O(N) 이지만 시간복잡도를 개선한 소수 판별법처럼 O(√N)으로 줄일 수 있다.

i의 루프 범위를 i <= N이 아닌 i*i <= N로 잡고 N이 1이 아닐 경우 그 수를 소인수 목록에 추가해주면 된다.

 

더보기
int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	cin >> n;
	
	for (int i = 2; i * i <= n; ++i)
	{
		while (n % i == 0)
		{
			cout << i << '\n';
			n /= i;
		}
	}
	if (n != 1)
		cout << n;

	return 0;
}

 

최대 공약수와 최소 공배수

 

약수 : 어떤 수를 나누어 떨어지게 하는 수

더보기
vector<int> divisor(int n) {
	vector<int> div;
	for (int i = 1; i * i <= n; ++i) {
		if (n % i == 0) div.push_back(i);
	}
	for (int j = (int)div.size() - 1; j >= 0; --j) {
		if (div[j] * div[j] == n) continue;
		div.push_back(n / div[j]);
	}
	return div;
}

 

시간복잡도는 O(√N) 이다.

 

최대 공약수(GCD) : 두 자연수의 공통된 약수 중 가장 큰 약수

유클리드 호제법을 이용하면 매우 빠르고 간단하게 구할 수 있다.

두 수 A, B에 대해 A를 B로 나눈 나머지를 r이라고 하면 GCD(A,B) = GCD(B,r)이다.

더보기
int gcd(int a, int b) {
	if (a == 0) return b;
	return gcd(b, a%b);
}

 

최소 공배수(LCM)

A×B = GCD(A,B)×LCM(A,B)

더보기
int lcm(int a, int b) {
	return a / gcd(a, b) * b;
}

 

본래의 식은 (a*b) / gcd(a,b) 이지만 a,b의 값이 10^9라면 a*b는 int overflow가 발생하기 때문에 예방차원에서 계산의 순서를 바꾼 것이다.

 

연립합동방정식

 

이항계수

 

nCk = (n-1)C(k-1) + (n-1)Ck

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x14] 투 포인터  (0) 2022.08.05
[0x13] 이분탐색  (0) 2022.08.05
[0x11] 그리디  (0) 2022.08.04
[0x10] 다이나믹 프로그래밍  (0) 2022.08.03
[0x0F] 정렬 II  (0) 2022.08.02

지금 가장 최적인 답을 근시안적으로 택하는 알고리즘.

관찰을 통해 탐색 범위를 줄이는 알고리즘 이라고도 볼 수 있다.

병합 정렬도 그리디의 일종이다.

 

이상적인 풀이 흐름

 

1. 관찰을 통해 탐색 범위를 줄이는 방법을 고안한다.

2. 탐색 범위를 줄여도 올바른 결과를 낸다는 사실을 수학적으로 증명한다.

3. 구현해서 문제를 통과한다.

 

코딩테스트에서의 추천 전략

 

거의 똑같은 문제를 풀어봤거나 간단한 문제여서 나의 그리디 풀이를 100% 확신한다

-> 구현 후 제출해보고 틀리면 바로 손절

 

100% 확신은 없지만 맞는 것 같은 그리디 풀이를 찾았다

-> 일단 넘어가고 다른 문제를 풀게 없거나 종료까지 20~40분정도 남은 시점에 코딩을 시작

 

그리디 문제를 못 푸는 것 보다 오히려 그리디로 푸는 문제가 아닌데 그리디로 풀 수 있다고 착각하는걸 더 조심해야 한다.

 

귀류법으로 명제의 참거짓 여부를 판단하는것이 좋다.

지금 당장 손해를 보더라도 나중에 가서 이득인 경우가 있을 수는 없는지 고민해보면 정당성을 확인하거나 반례를 찾을 때 도움이 된다.

 

연습 문제

 

11047: 동전 0

더보기
#include <iostream>

using namespace std;

int arr[11];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, k, answer = 0;
	cin >> n >> k;

	for (int i = 0; i < n; ++i)
		cin >> arr[i];

	for (int i = n - 1; i >= 0; --i)
	{
		answer += k / arr[i];
		k %= arr[i];
	}

	cout << answer;

	return 0;
}

 

1931: 회의실 배정

더보기
#include <iostream>
#include <algorithm>

using namespace std;

pair<int, int> arr[100001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, answer = 0, t = 0;
	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		// custom cmp 함수를 안만들기 위해 종료시간을 first로 둠
		cin >> arr[i].second >> arr[i].first;
	}

	sort(arr, arr + n);

	for (int i = 0; i < n; ++i)
	{
		if (t > arr[i].second)
			continue;
		t = arr[i].first;
		answer++;
	}

	cout << answer;

	return 0;
}

 

종료시간이 짧은 순으로 정렬해야 하기 때문에 cmp함수를 따로 만들지 않기 위해 first와 second를 반대로 입력 받았다.

 

2217: 로프

더보기
#include <iostream>
#include <algorithm>

using namespace std;

int arr[100001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, answer = 0;
	cin >> n;

	for (int i = 0; i < n; ++i)
		cin >> arr[i];
	
	sort(arr, arr + n);

	// 현재 로프의 최대중량 무게에서 로프 개수만큼 곱하면 로프가 버틸수 있는 최대무게가 됨
	for (int i = 1; i <= n; ++i)
		answer = max(answer, arr[n - i] * i);

	cout << answer;

	return 0;
}

 

n개의 로프에서 k개를 고른다고 하면 최대중량이 높은걸 k개 고르면 된다.

 

1026: 보물

더보기
#include <iostream>
#include <algorithm>

using namespace std;

int arrA[51];
int arrB[51];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, answer = 0;
	cin >> n;

	for (int i = 0; i < n; ++i)
		cin >> arrA[i];
	for (int i = 0; i < n; ++i)
		cin >> arrB[i];

	sort(arrA, arrA + n);
	sort(arrB, arrB + n);

	for (int i = 0; i < n; ++i)
		answer += arrA[i] * arrB[n-i-1];

	cout << answer;

	return 0;
}

 

재배열 부등식으로 증명이 가능하다.

큰 원소와 큰 원소를 매칭 시켜주면 결과가 최대가 되고, 큰 원소와 작은 원소를 매칭 시켜주면 결과가 최소가 된다는 부등식이다.

 

그리고 문제의 조건에 "단, B에 있는 수는 재배열하면 안 된다." 라고 되어있는데 전혀 상관이 없다.

A만 재배열할 때의 답을 구하는 것이 A,B 둘 다 재배열할 때의 답을 구하는 것과 동치임을 알아낼 수 있는가에 대한 것을 시험하는 것도 포함되어 있다고 볼 수 있다.

 

문제에서 특정 라이브러리를 쓰지 못하게 막거나 시간, 메모리에 제한을 두는것 빼고는 특정 방법을 사용해서 풀라고 강요하지 않고 강요할 방법도 없다.

 

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x13] 이분탐색  (0) 2022.08.05
[0x12] 수학  (0) 2022.08.05
[0x10] 다이나믹 프로그래밍  (0) 2022.08.03
[0x0F] 정렬 II  (0) 2022.08.02
[0x0E] 정렬 I  (0) 2022.08.02

DP를 푸는 과정

 

1. 테이블 정의하기

2. 점화식 찾기

3. 초기값 정하기

 

코딩테스트에 나올 수준의 DP는 일단 점화식만 찾고나면 그 뒤는 초기값을 채워넣은 후에 반복문을 돌면서 배열을 채워넣으면 끝이다.

하지만 그 점화식을 찾는게 가장 어렵다.

주어진 문제가 DP로 풀 수 있는 문제라는 것 자체를 눈치채지 못할 수도 있다.

이 부분은 그저 많이 풀면서 테이블을 잡고 식을 세우는 과정을 반복학습 하는 수 밖에 없다.

 

연습 문제

 

1463: 1로 만들기

더보기

BFS로 구현할수도 있다. 하지만 DP를 이용하면 더 짧막하게 구현할 수 있다.

 

1. 테이블 정의하기

D[i] = i를 1로 만들기 위해 필요한 연산 사용 횟수의 최소값

 

2. 점화식 찾기

예를들어 D[12] 를 찾는다고 하면 총 3가지의 행동을 할 수 있다.

  • 3으로 나누거나 (D[12] = D[4] + 1)
  • 2로 나누거나 (D[12] = D[6] + 1)
  • 1을 빼거나 (D[12] = D[11] + 1)

 

최소값을 찾는것이기 때문에 D[12] = min(D[4]+1, D[6]+1, D[11]+1)이 성립하게 된다.

즉, D[K] = min(D[K/3]+1, D[K/2]+1, D[K-1]+1) 이다.

 

3. 초기값 정의하기

D[1] = 0

 

#include <iostream>
#include <algorithm>

using namespace std;

int arr[1000001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	cin >> n;

	for (int i = 2; i <= n; ++i)
	{
		arr[i] = arr[i - 1] + 1;
		if (i % 2 == 0) arr[i] = min(arr[i], arr[i / 2] + 1);
		if (i % 3 == 0) arr[i] = min(arr[i], arr[i / 3] + 1);
	}

	cout << arr[n];

	return 0;
}

 

9095: 1, 2, 3 더하기

더보기

1. 테이블 정의하기

D[i] = i를 1, 2, 3의 합으로 나타내는 방법의 수

 

2. 점화식 찾기

D[4]인 경우,

1+1+1+1, 3+1, 2+1+1, 1+2+1 / (3을 1, 2, 3의 합으로 나타내는 방법) + 1, D[3]

1+1+2, 2+2 / (2를 1, 2, 3의 합으로 나타내는 방법) + 2, D[2]

1+3 / (1을 1, 2, 3의 합으로 나타내는 방법) + 3, D[1]

D[4] = D[1] + D[2] + D[3]

D[i] = D[i-1] + D[i-2] + D[i-3]

 

3. 초기값 정의하기

D[1] = 1, D[2] = 2, D[3] = 4

초기값이 3개는 주어져야 한다.

 

#include <iostream>

using namespace std;

int arr[12];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int tc, n;
	cin >> tc;

	// 미리 테이블을 구해두고 바로 출력해준다
	arr[1] = 1;
	arr[2] = 2;
	arr[3] = 4;

	for (int i = 4; i <= 11; ++i)
		arr[i] = arr[i - 3] + arr[i - 2] + arr[i - 1];

	while (tc--)
	{
		cin >> n;
		cout << arr[n] << '\n';
	}

	return 0;
}

 

2579: 계단 오르기

더보기

1. 테이블 정의하기

(X) D[i] = i번째 계단까지 올라섰을 때 점수 합의 최대값 (연속해서 3개의 계단을 밟을수 없다는 조건때문에 사용할 수 없음)

(O) D[i][j] = 현재까지 j개의 계단을 연속해서 밟고 i번째 계단까지 올라섰을 때 점수 합의 최대값. i번째 계단은 반드시 밟아야 함

 

2. 점화식 찾기

  • D[k][1] = max(D[k-2][1], D[k-2][2]) + S[k]
  • D[k[2] = D[k-1][1] + S[k]

 

3. 초기값 정의하기

D[1][1] = S[1], D[1][2] = 0

D[2][1] = S[2], D[2][2] = S[1] + S[2]

 

#include <iostream>

using namespace std;

int arr[301];
int psum[301][3];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	cin >> n;

	for (int i = 1; i <= n; ++i)
		cin >> arr[i];

	psum[1][1] = arr[1];
	psum[1][2] = 0; // 현재 계단은 한개밖에 없다
	psum[2][1] = arr[2]; // 2층까지 한개를 연속해서 밟았다면 k-1개는 건너뛰었다는 뜻
	psum[2][2] = arr[1] + arr[2]; // 2층까지 두개를 연속해서 밟았다면 k-1, k 둘 다 밟았다는 뜻

	for (int i = 3; i <= n; ++i)
	{
		// i번째 계단이 연속해서 한개째 밟고 있다면 i-1번째 계단은 건너뛰고 i-2번째 계단을 밟았다는 뜻
		psum[i][1] = max(psum[i - 2][1], psum[i - 2][2]) + arr[i];
		// i번째 계단이 연속해서 두개째 밟고있는 계단이라면 i-1번째 계단을 밟았다는 뜻. 연속으로 3개는 밟지 못하므로 i-2 또는 i-1의 연속2개는 안된다
		psum[i][2] = psum[i - 1][1] + arr[i];
	}

	cout << max(psum[n][1], psum[n][2]);

	return 0;
}

 

1149: RGB거리

더보기

1. 테이블 정의하기

D[i][0] = i번째 집까지 칠할 때 비용의 최소값, i번째 집은 빨강

D[i][1] = i번째 집까지 칠할 때 비용의 최소값, i번째 집은 초록

D[i][2] = i번째 집까지 칠할 때 비용의 최소값, i번째 집은 파랑

 

2. 점화식 찾기

  • D[i][0] = min(D[i-1][1], D[i-1][2]) + R[i]
  • D[i][1] = min(D[i-1][0], D[i-1][2]) + G[i]
  • D[i][2] = min(D[i-1][0], D[i-1][1]) + B[i]

 

3. 초기값 정의하기

D[1][0] = R[1]

D[1][1] = G[1]

D[1][2] = B[1]

 

최종적으로 D[N]중 가장 작은값을 출력하면 된다.

 

#include <iostream>
#include <algorithm>

using namespace std;

int arr[1001][3];
int r[1001], g[1001], b[1001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, answer = 0;
	cin >> n;

	for (int i = 1; i <= n; ++i)
	{
		cin >> r[i] >> g[i] >> b[i];
	}

	arr[1][0] = r[1];
	arr[1][1] = g[1];
	arr[1][2] = b[1];

	for (int i = 2; i <= n; ++i)
	{
		arr[i][0] = min(arr[i-1][1], arr[i-1][2]) + r[i];
		arr[i][1] = min(arr[i-1][0], arr[i-1][2]) + g[i];
		arr[i][2] = min(arr[i-1][0], arr[i-1][1]) + b[i];
	}

	cout << *min_element(arr[n], arr[n]+3);

	return 0;
}

 

11726: 2×n 타일링

더보기

1. 테이블 정의하기

D[i] = 2×i 크기의 직사각형을 채우는 방법의 수

 

2. 점화식 찾기

  • 2×1 크기의 타일을 하나 놓았을 때 나머지 타일을 채우는 방법의 수는 D[i-1]
  • 1×2 크기의 타일을 하나 놓았을 때, 아래쪽 공간에는 똑같은 1×2 타일만 들어갈 수 있다. 이 때, 나머지 타일을 채우는 방법의 수는 D[i-2]

D[i] = D[i-1] + D[i-2]

 

3. 초기값 정의하기

D[1] = 1, D[2] = 2

 

#include <iostream>

using namespace std;

int arr[1001];
int mod = 10007;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	cin >> n;

	arr[1] = 1;
	arr[2] = 2;

	for (int i = 3; i <= n; ++i)
		arr[i] = (arr[i - 1] + arr[i - 2]) % mod; // 매번 mod의 나머지값만 가져가면 된다

	cout << arr[n];

	return 0;
}

 

11659: 구간 합 구하기 4

더보기

1. 테이블 정의하기

D[i] = A[1] + A[2] + ... + A[i] = D[i-1] + A[i]

 

2. 점화식 찾기

  • D[j] - D[i-1] = (A[1] + A[2] + .. A[j]) - (A[1] + A[2] + .. + A[i-1)

 

3. 초기값 정의하기

D[0] = 0

 

#include <iostream>

using namespace std;

int arr[100001];
int prefixSum[100001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, m;
	cin >> n >> m;

	for (int i = 1; i <= n; ++i)
	{
		cin >> arr[i];
		prefixSum[i] = prefixSum[i - 1] + arr[i];
	}

	while (m--)
	{
		int i, j;
		cin >> i >> j;
		cout << prefixSum[j] - prefixSum[i - 1] << '\n';
	}

	return 0;
}

 

 

기본 문제

 

1003: 피보나치 함수

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

vector<pair<int, int>>v(41);

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int tc;
	cin >> tc;
	v[0] = { 1, 0 };
	v[1] = { 0, 1 };
	for (int i = 2; i <= 40; ++i)
		v[i] = { v[i - 1].first + v[i - 2].first, v[i - 1].second + v[i - 2].second };
	while (tc--)
	{
		int n;
		cin >> n;
		cout << v[n].first << ' ' << v[n].second << '\n';
	}

	return 0;
}

 

1932: 정수 삼각형

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

int t[501][501];
int n;

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 1; j <= i; ++j)
			cin >> t[i][j];
	}

	for (int i = n-1; i > 0; --i)
	{
		for (int j = 1; j <= i; ++j)
			t[i][j] = max(t[i + 1][j], t[i + 1][j + 1]) + t[i][j];
	}
	cout << t[1][1];
	
	return 0;
}

 

비재귀 top-down으로 깔끔하게 구현이 됨

처음에는 bottom-down으로 하려다가 도무지 답이 안나와서 구글링의 도움을 받았다

 

11727: 2×n 타일링 2

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

int n, mod = 10007;
int t[1001];

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	
	t[1] = 1;
	t[2] = 3;
	for (int i = 3; i <= n; ++i)
		t[i] = (t[i - 1] + t[i - 2] * 2) % mod;
	cout << t[n];

	return 0;
}

 

2193: 이친수

더보기
using namespace std;
using ll = long long;
int n;
ll t[91];

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;
	t[0] = 1; t[1] = 1; t[2] = 1;
	for (int i = 3; i <= n; ++i)
	{
		for (int j = 0; j <= i - 2; ++j) t[i] += t[j];
	}
	cout << t[n];

	return 0;
}

 

나름 자력으로 풀었음

 

1912: 연속합

더보기
#include <bits/stdc++.h>
using namespace std;
int n, mx, ans;
int t[100001];

int main(void)
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n;
	for (int i = 0; i < n; ++i) cin >> t[i];

	mx = ans = t[0];
	for (int i = 1; i < n; ++i)
	{
		mx = max(t[i], t[i] + mx);
		ans = max(ans, mx);
	}
	cout << ans;
	return 0;
}

 

절반정도는 접근했다가 생각의 늪에 빠져서 못풀고 구글링했음

최대값들 중의 최대값을 선별해야됨

 

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x12] 수학  (0) 2022.08.05
[0x11] 그리디  (0) 2022.08.04
[0x0F] 정렬 II  (0) 2022.08.02
[0x0E] 정렬 I  (0) 2022.08.02
[0x0D] 시뮬레이션  (0) 2022.08.01

카운팅 정렬 (Counting Sort)

 

수의 범위가 어느정도 한정적일 때에만 사용할 수 있다.

대략적으로 수의 범위가 천만 이하일때 사용한다고 보면 된다.

 

15688: 수 정렬하기 5

더보기
#include <iostream>

using namespace std;

int csarr[2000001];
int n;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		int t;
		cin >> t;
		csarr[t + 1000000]++;
	}

	for (int i = 0; i < 2000001; ++i)
	{
		while (csarr[i]--)
		{
			cout << i - 1000000 << '\n';
		}
	}

	return 0;
}

 

기수 정렬 (Radix Sort)

 

자릿수를 이용해서 정렬을 수행하는 알고리즘.

각 자리수에 대해 카운팅 정렬을 수행하는 알고리즘이라고 볼 수도 있다.

자릿수가 D일때 시간복잡도는 O(DK)이다.

 

기수 정렬의 최대 단점은 N의 크기에 따라 공간 복잡도가 비례해서 증가한다는 점이다.

이 문제를 어느정도 해결하려면 각 리스트를 vector 또는 연결 리스트로 사용해야 하는데 vector나 연결 리스트나 둘다 STL이 없으면 구현이 많이 까다롭고, STL을 쓸수 있으면 STL sort 함수를 쓰는게 모든면에서 낫다.

 

한마디로 구현을 해야 할 상황이 아예 존재하지 않는다. 개념만 이해하고 STL을 쓸 수 없는 상황에서는 병합 정렬이나 힙 정렬을 이용하면 된다.

 

STL sort

 

퀵 정렬을 기반으로 하지만 리스트가 불균등하게 쪼개지는 상황이 반복되어 재귀의 깊이가 너무 깊어지면 O(NlogN)이 보장되는 알고리즘으로 나머지를 처리한다. 그렇기 때문에 최악의 경우에도 O(NlogN)이 보장된다.

퀵 정렬을 기반으로 한 만큼 Unstable Sort이다.

꼭 Stable Sort가 필요하다면 stable_sort 를 사용하면 된다.

 

pair, tuple을 정렬하는 경우 앞의 원소의 대소를 비교하고 값이 같으면 그 뒤의 원소의 대소를 비교하는 방식으로 처리한다.

 

비교 함수를 내가 정해서 넘겨줄 수도 있다.

 

비교 함수는 두 값이 같을 때(또는 우선순위가 같을 때) 반드시 false를 반환해주어야 한다.

인자로 STL 또는 객체 전달시 reference를 사용해야 한다. 그래야 불필요한 공간을 사용하지 않기 때문이다.

 

custom compare 함수를 직접 만들 때, 2개의 인자를 받을 수 있다.

순서대로 n+1, n이며 return이 true인 경우만 값의 교환이 일어난다.

 

번외 : Comparison Sort, Non-comparison Sort

 

버블, 병합, 퀵 정렬은 Comparison Sort

카운팅, 기수 정렬은 Non-comparison Sort

 

코딩 테스트에서 STL 사용을 의도적으로 막지 않은 이상, 정렬을 구현하고 앉아있으면 호구다.

 

연습 문제

 

1431: 시리얼 번호

더보기
#include <iostream>
#include <algorithm>
#include <string>

using namespace std;

string s[50];

// a는 n+1, b는 n이 들어감
// true인 경우만 교환이 이뤄진다
bool comp(const string& a, const string& b)
{
	if (a.length() != b.length())
	{
		return a.length() < b.length();
	}
	else if (a.length() == b.length())
	{
		int asum = 0;
		int bsum = 0;

		for (auto i = 0; i < a.length(); ++i)
		{
			if (isdigit(a[i])) asum += a[i] - '0';
			if (isdigit(b[i])) bsum += b[i] - '0';
		}
		if (asum != bsum)
			return asum < bsum;
	}
	return a < b;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;

	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		cin >> s[i];
	}

	sort(s, s + n, comp);

	for (int i = 0; i < n; ++i)
	{
		cout << s[i] << '\n';
	}

	return 0;
}

 

custom compare 함수의 인자를 뭘로 받았는지, true/false시 어떻게 돌아가는지 몰라서 조금 찾아보고 금방 이해함

 

11652: 카드

더보기
#include <iostream>
#include <algorithm>

using namespace std;

using ll = long long;
ll arr[100001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	int cnt = 1, mxcnt = 1;
	ll mxval;

	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		cin >> arr[i];
	}

	sort(arr, arr + n);

	arr[n] = (1 << 62) + 1;

	mxval = arr[0];

	for (int i = 0; i < n; ++i)
	{
		if (arr[i] == arr[i + 1])
		{
			cnt++;
		}
		else
		{
			if (mxcnt < cnt)
			{
				mxcnt = cnt;
				mxval = arr[i];
			}
			cnt = 1;
		}
	}

	cout << mxval;

	return 0;
}

 

기본 문제

 

5648: 역원소 정렬

더보기
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;
using ll = long long;

vector<ll> arr;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;

	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		string t;
		cin >> t;
		reverse(t.begin(), t.end());
		arr.push_back(stoll(t));
	}

	sort(arr.begin(), arr.end());

	for (auto& item : arr)
	{
		cout << item << '\n';
	}

	return 0;
}

 

 1181: 단어 정렬

더보기
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

vector<string> arr;

bool comp(const string& a, const string& b)
{
	if (a.length() != b.length())
	{
		return a.length() < b.length();
	}

	return a < b;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;

	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		string t;
		cin >> t;
		arr.push_back(t);
	}

	sort(arr.begin(), arr.end(), comp);

	for (int i = 0; i < n - 1; ++i)
	{
		if (arr[i] == arr[i + 1]) continue;
		cout << arr[i] << '\n';
	}
	cout << arr.back();

	return 0;
}

 

 2910: 빈도 정렬

더보기
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<pair<int, int>> v;

bool comp(const pair<int, int>& a, const pair<int, int>& b)
{
	return a.second > b.second;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n, c;

	cin >> n >> c;

	for (int i = 0; i < n; ++i)
	{
		int t;
		cin >> t;

		// find를 사용시 pair 구조체의 멤버함수에 operator== 가 정의되어 있지 않아서 문제가 생긴다.
		// 이런경우 find_if로 조건을 직접 만든 메소드를 인자로 넘겨줘야 한다
		// sort의 custom compare와 같은 개념이다
		auto iter = find_if(v.begin(), v.end(), [&t](const pair<int, int>& elem) { return elem.first == t; });

		if (iter != v.end())
		{
			(*iter).second += 1;
		}
		else
		{
			v.push_back({ t, 1 });
		}
	}

	stable_sort(v.begin(), v.end(), comp);

	for (auto& elem : v)
	{
		for (int i = 0; i < elem.second; ++i)
		{
			cout << elem.first << ' ';
		}
	}

	return 0;
}

 

람다식과 stable_sort를 같이 사용함

 

10814: 나이순 정렬

더보기
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

vector<pair<int, string>> arr;

bool comp(const pair<int, string>& a, const pair<int, string>& b)
{
	return a.first < b.first;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;

	cin >> n;

	for (int i = 0; i < n; ++i)
	{
		int t;
		string ts;
		cin >> t >> ts;
		arr.push_back({ t, ts });
	}

	stable_sort(arr.begin(), arr.end(), comp);

	for (auto& data : arr)
	{
		cout << data.first << ' ' << data.second << '\n';
	}

	return 0;
}

 

stable_sort만 주의하면 큰 문제 없다.

 

11656: 접미사 배열

더보기
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> v;

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	string s;

	cin >> s;

	for (auto i = 0; i < s.length(); ++i)
		v.push_back(s.substr(i, s.length()-i));

	sort(v.begin(), v.end());

	for (auto& elem : v)
		cout << elem << '\n';

	return 0;
}

 

string의 substr 사용. 해당 문자열의 1번째 인자 시작 위치부터 2번째 인자의 갯수만큼 반환한다.

 

10825: 국영수

더보기
#include <iostream>
#include <string>
#include <vector>
#include <tuple>
#include <algorithm>

using namespace std;
using Elements = pair<string, tuple<int, int, int>>;

vector<Elements> v;

bool comp(const Elements& a, const Elements& b)
{
	if (get<0>(a.second) != get<0>(b.second))
		return get<0>(a.second) > get<0>(b.second);
	else if (get<1>(a.second) != get<1>(b.second))
		return get<1>(a.second) < get<1>(b.second);
	else if (get<2>(a.second) != get<2>(b.second))
		return get<2>(a.second) > get<2>(b.second);
	return a.first < b.first;
}

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int n;
	cin >> n;

	while (n--)
	{
		string name;
		int lan, eng, mat;

		cin >> name >> lan >> eng >> mat;

		v.push_back({ name, {lan, eng, mat} });
	}

	sort(v.begin(), v.end(), comp);

	for (auto& elem : v)
		cout << elem.first << '\n';

	return 0;
}

 

comp함수의 조건을 잘 조절하는게 포인트

 

응용 문제

 

7795: 먹을 것인가 먹힐 것인가

더보기
#include <iostream>
#include <algorithm>

using namespace std;

int arrA[20001];
int arrB[20001];

int main()
{
	ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

	int tc;
	cin >> tc;

	while (tc--)
	{
		int n, m;
		size_t answer = 0;

		cin >> n >> m;

		for (int i = 0; i < n; ++i)
			cin >> arrA[i];
		for (int i = 0; i < m; ++i)
			cin >> arrB[i];

		sort(arrB, arrB + m);

		for (int i = 0; i < n; ++i)
			answer += lower_bound(arrB, arrB + m, arrA[i]) - arrB;
		cout << answer << '\n';
	}

	return 0;
}

 

바킹독님 얘기로는 이분 탐색을 쓰지 않고 정렬만으로도 풀수 있다고 했다.

나중에 고민해보기로.

'알고리즘 > 바킹독의 실전 알고리즘' 카테고리의 다른 글

[0x11] 그리디  (0) 2022.08.04
[0x10] 다이나믹 프로그래밍  (0) 2022.08.03
[0x0E] 정렬 I  (0) 2022.08.02
[0x0D] 시뮬레이션  (0) 2022.08.01
[0x0C] 백트래킹  (0) 2022.07.31

+ Recent posts