이진탐색은 탐색하고자 하는 범위의 시작점, 끝점, 중간점을 지정하여 찾으려는 데이터와 중간점 위치에 있는 데이터를 반복적으로 비교해서 원하는 데이터를 찾는 탐색방법이다.
아래의 그림은 이진탐색트리에서 37을 탐색하는 과정이다.
(start+end)/2
로 중간값인 mid를 구한다mid+1
,끝
으로 범위를 수정한다시작지점
, mid-1
로 범위를 수정한다#include <iostream>
#include <algorithm>
using namespace std;
int arr[1000001];
int parts[100001];
int n, m;
void search_parts(int start, int end, int part)
{
int mid = (start + end) / 2;
if (start > end)
{
cout << "no" << "\n";
return;
}
if (arr[mid] < part)
search_parts(mid + 1, end, part);
else if (arr[mid] > part)
search_parts(start, mid - 1, part);
else
{
cout << "yes" << "\n";
return;
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
cin >> m;
for (int i = 0; i < m; i++)
{
cin >> parts[i];
}
sort(arr, arr + n);
for (int i = 0; i < m; i++)
{
search_parts(0, n - 1, parts[i]);
}
}
순차탐색을 이용한 로직은 떠올렸고 답도 멀쩡히 나오지만 이진탐색 파트에 있는 것이 걸리는 문제였다. 잘린 떡의 합을 구하는데 O(N)이고 최악의 경우 O(N)을 배열 안의 가장 큰 값만큼 반복할 수 있다. 따라서 입력받는 자료가 커질수록 복잡도가 높아지는 것을 예상할 수 있었다. 심지어 절단기 높이가 10억까지의 정수라는 것 때문에 이 코드가 틀렸다는 것을 직감했다. 그래서 복잡도를 낮추기 위해 책에서 제공하는 코드를 참고하고 이해했다.
<순차탐색>
<이진탐색>
수정 전
#include <iostream>
using namespace std;
int arr[1000001];
int main()
{
int n, m, leng, sum, ans;
cin >> n >> m;
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
leng = 1;
while(1)
{
sum = 0;
for (int i = 0; i < n; i++)
{
if (arr[i] - leng >= 0)
sum += arr[i] - leng;
}
if (sum < m)
{
cout << leng - 1 << "\n";
return 0;
}
else
leng++;
}
return 0;
}
수정 후
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> arr;
int main(void) {
cin >> n >> m;
for (int i = 0; i < n; i++)
{
int x;
cin >> x;
arr.push_back(x);
}
int start = 0;
int end = 1e9;
int result = 0;
while (start <= end) {
long long int total = 0;
int mid = (start + end) / 2;
for (int i = 0; i < n; i++)
{
if (arr[i] > mid)
total += arr[i] - mid;
}
if (total < m)
{
end = mid - 1;
}
else {
result = mid;
start = mid + 1;
}
}
cout << result << '\n';
}
재귀로 풀었다. 스터디 준비하면서 dfs/bfs풀 때 비슷한 유형의 문제들을 많이 겪어봤는데 덕분에 로직은 쉽게 떠올랐다.
+a)이 문제는 dfs문제랑 결은 비슷하지만 dfs는 아니고 분할정복 문제라고 한다. 얏호
주석은 코드 수정이 필요해서 사용했다.
#include <iostream>
using namespace std;
int arr[129][129];
int n;
int b = 0;
int w = 0;
void dfs(int x, int y, int leng)
{
bool blue, white;
blue = white = true;
if (leng == 1)
{
if (arr[x][y] == 1)
b++;
else
w++;
return;
}
for (int i = x; i < x + leng; i++)
{
for (int j = y; j < y + leng; j++)
{
if (arr[i][j] == 1)
white = false;
if (arr[i][j] == 0)
blue = false;
}
}
if (white == true)
{
w++;
//cout << "하얀색이 추가됨" << endl;
return;
}
if (blue == true)
{
b++;
//cout << "파란색이 추가됨" << endl;
return;
}
int i = leng / 2;
dfs(x, y, i);
dfs(x, y + i, i);
dfs(x + i, y, i);
dfs(x + i, y + i, i);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
cin >> arr[i][j];
}
dfs(0, 0, n);
cout << w << "\n" << b << "\n";
return 0;
}
다이나믹 프로그래밍은 하나의 큰 문제를 해결하기 위해 풀리는 작은 문제들이 서로 연결된 경우를 의미한다. 따라서 인접한 항들 사이의 관계식인 점화식을 이용한다. 메모이제이션도 dp를 위한 기법 중 하나이지만 메모이제이션 한 자료를 다시 사용해야 dp라고 할 수 있고 단순히 저장만 하는 경우 dp가 아니다. 다이나믹 프로그래밍에는 바텀업 방식과 탑다운 방식이 있다.
반복문
DP 테이블
작은 문제를 이용해서 큰 문제 해결
재귀함수
메모이제이션
큰 문제에서 시작해서 작은 문제들을 해결
책에서는 재귀함수의 스택버퍼 문제로 바텀업 방식을 추천한다.
이 문제와 비슷한 문제를 백준에서 풀어본 기억이 있다. 그 때의 로직을 되살려서 코드를 작성했으나 이전의 문제와 조건이 달라서 한번 코드 순서를 뒤집어서 짰다. 교재에 있는 로직을 참고했는데 너무 깔끔한 로직이라 감탄함.
수정 전
#include <iostream>
#include <algorithm>
using namespace std;
int arr[30001];
int main()
{
int x;
cin >> x;
arr[2] = 1;
if (x > 2)
{
for (int i = 3; i <= x; i++)
{
if (i % 2 == 0 || i % 3 == 0 || i % 5 == 0)
{
arr[i] = min(arr[i / 2] + 1, arr[i / 3] + 1);
arr[i] = min(arr[i], arr[i / 5] + 1);
}
else if (i % 5 == 0)
arr[i] = min(arr[i / 5] + 1, arr[i - 1] + 1);
else if (i % 3 == 0)
arr[i] = min(arr[i / 3] + 1, arr[i - 1] + 1);
else if (x % 2 == 0)
arr[i] = min(arr[i / 2] + 1, arr[i - 1] + 1);
else
arr[i] = arr[i - 1] + 1;
}
}
cout << arr[x] << "\n";
}
수정 후
#include <iostream>
using namespace std;
int arr[30001];
int main()
{
int x;
cin >> x;
arr[2] = 1;
if (x > 2)
{
for (int i = 3; i <= x; 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);
if (i % 5 == 0)
arr[i] = min(arr[i], arr[i / 5] + 1);
}
}
cout << arr[x] << "\n";
}
이동할 때마다 현재 위치의 식량 총합을 받는 배열을 만든다는 아이디어는 책을 보고 알았다. 책의 로직대로 sum[0]과 sum[1]의 값을 미리 넣고 그 이후에 다음 값들을 예상해보면 sum[2]
는 sum[0]+arr[2]
와 sum[1]
중 더 큰 값으로 결정된다. 이걸 점화식으로 표현하면 sum[i]=max(sum[i-1], sum[i-2]+arr[i])
가 된다.
#include <iostream>
using namespace std;
int arr[101];
int sum[101];
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
sum[0] = arr[0];
sum[1] = max(arr[0], arr[1]);
for (int i = 2; i < n; i++)
{
sum[i] = max(sum[i - 1], sum[i - 2] + arr[i]);
}
cout << sum[n - 1] << "\n";
return 0;
}
머리로는 받아들일 수 있는데 이걸 직접 점화식 생각해내려고 하니 도저히 떠오르는게 없었다. 실전에서 이 문제를 어떻게 풀지? 코드는 정말 간단했는데 위의 문제들과는 달리 해설을 봐도 이해만 될뿐 다음번 풀이에 응용하지 못할 것 같다.
#include <iostream>
using namespace std;
int arr[1001];
int main()
{
int x;
cin >> x;
arr[1] = 1;
arr[2] = 3;
for (int i = 3; i <= x; i++)
{
arr[i] = arr[i - 1] + arr[i - 2] * 2;
}
cout << arr[x] << "\n";
}
이해중
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> arr;
int main(void)
{
cin >> n >> m;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
arr.push_back(x);
}
vector<int> d(m + 1, 10001);
d[0] = 0;
for (int i = 0; i < n; i++)
{
for (int j = arr[i]; j <= m; j++)
{
if (d[j - arr[i]] != 10001)
d[j] = min(d[j], d[j - arr[i]] + 1);
}
}
if (d[m] == 10001)
{
cout << -1 << '\n';
}
else {
cout << d[m] << '\n';
}
}
dp 카테고리에서 찾은 문제였는데 점화식은 찾았으나 입증할 방법이 없어서 조금 더 신빙성이 있는 dfs로 풀었다.
내가 찾은 점화식은 n이 4이상일 때 dp(n)=dp(n-1)+dp(n-2)+dp(n-3)
이었는데 아마 dp 카테고리에 있으니 맞을거 같다.
dfs는 입력받은 n을 최대 버퍼로 생각하고 1, 2, 3을 n이 될 때까지 더해줬다. 반복문을 돌릴 때 주의한 부분은 1, 2, 3을 더했을 때 n을 넘는 경우가 있으므로 그 경우는 조건문을 걸어서 처리했다.
#include <iostream>
using namespace std;
int N;
int cnt=0;
void dfs(int n)
{
if(n==N)
{
cnt++;
return;
}
for(int i=1;i<=3;i++)
{
if(n+i<=N)
dfs(n+i);
}
}
int main()
{
int number;
cin>>number;
for(int i=0;i<number;i++)
{
cin>>N;
dfs(0);
cout<<cnt<<"\n";
cnt=0;
}
return 0;
}