반응형

# 14. Longest Common Prefix

String 배열이 주어졌을 때 가장 긴 Common Prefix 를 찾는 문제

 

Common Prefix 를 찾는 방법으로 생각한 것은, 우선 문자열들을 길이 순서대로 정렬해서 가장 짧은 문자열을 공통 Prefix 로 먼저 저장을 한다. 그 다음에 순서대로 탐색을 하면서, 공통 Prefix 랑 문자열 하나씩 비교를 하고 비교했을 때 서로 같은 위치까지를 Prefix 로 저장한다. 이걸 반복하다 보면, Prefix 의 길이가 점차 짧아져서 탐색 / 비교 속도가 빠를 것이라고 생각된다.

 

string longestCommonPrefix(vector<string>& strs) {
	if (strs.empty()) {
    	return "";
    }
    
    sort(strs.begin(), strs.end());
    string prefix = strs[0];
    
    for (int i = 1; i < strs.size(); i++) {
		int l;
        for (l = 0; l < prefix.length(); l++) {
        	if (prefix[l] != strs[i][l]) {
            	break;
            }
        }
        if (l == 0) {
        	return "";
        }
        prefix = prefix.substr(0, l);
    }
    return prefix;
}

# 15. 3Sum

정수 배열이 주어졌을 때, A + B + C = 0 이 되는 Unique 한 {A, B, C} 조합을 찾는 문제

 

우선, 문제에서 주어진 배열의 Length 가 최대 3000 이라서, O(n^3) 탐색을 하게 되면 무조건 시간 초과일 것 같아서, 이걸 O(n^2) 으로 탐색할 방법을 생각하게 되었다. 그래서, A + B = 0 이 되는 두 수를 찾을 때 탐색을 최소화할 수 있는 방법을 생각해 보았는데 수를 정렬해 놓고 두 수의 합에 따라서 앞뒤 인덱스를 증가/감소 시켜가며 탐색하면 O(n) 만 탐색이 가능할 것 같았다. 그래서 세 수를 찾는 문제는, 한 수를 고정해 놓고 두 수를 찾는 방식으로 하면 고정하는 수 찾기 O(n) * 나머지 두 수 찾기 O(n) 해서 O(n^2) 에 탐색이 가능할 것으로 생각해 문제를 풀었다.

 

vector<vector<int>> threeSum(vector<int>& nums) {
	sort(nums.begin(), nums.end());
    vector<vector<int>> ans;
    
    for (int i = 0; i < nums.size(); i++) {
    	if (i > 0 && nums[i] == nums[i - 1]) {
        	continue; // 중복 제거
        }
        int left = i + 1, right = nums.size() - 1; // 왼쪽, 오른쪽 Index
        while (left < right) {
        	if (right < nums.size() - 1 && nums[right] == nums[right + 1]) {
            	right--;
                continue; // 마찬가지로 오른쪽의 값이 중복될 경우 중복 제거
            }
            
            int sum = nums[i] + nums[left] + nums[right];
            if (sum > 0) {
            	right--; // sum 이 0 보다 크면 right index 감소
            }
            else if (sum < 0) {
            	left++; // sum 이 0 보다 작으면 left index 증가
            }
            else {
            	vector<int> tmp = {nums[i], nums[left], nums[right]};
                ans.push_back(tmp);
                left++;
                right--;
            }
        }
    }
    return ans;
}

# 16. 3Sum Closest

이 문제는 #15. 3Sum 문제와 유사하지만, 차이점은 A + B + C = 0 인 위의 문제와 다르게, Target 값을 정해놓고 A + B + C = Target 에 가장 가까운 합을 구하는 데 있다.

 

그래서 이 문제를 풀 때는, 위와 마찬가지로 정렬을 하고 탐색도 동일한 방식으로 진행하지만, 세 수의 조합을 저장하지 않고 Target 값과 현재 Sum 의 차를 비교해서 기존의 차이보다 더 작게 차이가 나면 Sum 을 저장하는 방식으로 문제를 풀었다.

 

int _abs(int x) {
	return (x > 0 ? x : -x);
}

int threeSumClosest(vector<int>& nums, int target) {
	int diff = INT_MAX, ans = 0;
    sort(nums.begin(), nums.end());
    
    for (int i = 0; i < nums.size(); i++) {
    	if (i > 0 && nums[i] == nums[i - 1]){
        	continue; // 중복 제거
        }
        int left = i + 1, right = nums.size() - 1;
        while (left < right) {
        	if (right < nums.size() - 1 && nums[right] == nusm[right + 1]) {
            	k--;
                continue;
            }
            
            int sum = nums[i] + nums[left] + nums[right];
            if (_abs(sum - target) < diff) {
            	diff = _abs(sum - target);
                ans = sum;
            }
            if (sum > target) {
            	right--;
            }
            else if (sum < target) {
            	left++;
            }
            else {
            	return sum;
            }
        }
    }
    return ans;
}

'Algorithm > LeetCode' 카테고리의 다른 글

[LeetCode # 20 ~ 22] 문제 풀이  (0) 2021.03.18
[LeetCode # 17 ~ 19] 문제 풀이  (0) 2021.03.18
[LeetCode # 11 ~ 13] 문제 풀이  (0) 2021.02.28
[LeetCode # 7 ~ 9] 문제 풀이  (0) 2021.02.26
<LeetCode # 4 ~ 6> 문제 풀이  (0) 2021.02.23
반응형

# 11. Container With Most Water

일렬로 세워진 n 개의 기둥들의 높이가 주어졌을 때, 두 기둥 사이에 받을 수 있는 물의 최대양을 구하는 문제

* 두 기둥 사이에 받을 수 있는 물은 (두 기둥 사이 거리 * 두 기둥 중 낮은 기둥의 높이) 로 정의

 

처음 이 문제를 봤을 때는, O(n^2) 으로 경우를 다 탐색해야 하나 생각을 했는데 이렇게 할 경우에 n 이 최대 3만이고 n^2 은 9억이니 불가능할 것이라고 생각했다. 그래서 생각한 점은, 기둥 사이 거리의 최대는 양 끝점이고, 양 끝점부터 하나씩 좁혀나가며 탐색을 하면 n 번만 탐색을 할 수 있을 것이라고 생각이 들었다.

 

#define _MIN_(X, Y) ((X) < (Y) ? (X) : (Y))

int maxArea(vector<int> &height) {
	int l = 0, r = height.size() - 1;
    int max = 0;
    while (l < r) {
    	int area = _MIN_(height[l], height[r]) * (r - l);
        max = max > area ? max : area;
        if (height[l] < height[r])
        	l++;
        else
        	r--;
    }
}

# 12. Integer to Roman

정수 N 이 주어졌을 때, 이를 Roma 표기법으로 변경하는 문제

* I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000

 

이 문제는 풀이가 떠오르지 않아서 Solution 을 참고해 풀이를 작성했다.

나는 1, 5, 10, 50, ... 각 숫자 앞자리가 1 / 5 로 달라서 이를 어떻게 처리할 지 고민을 했는데, 솔루션은 이를 가볍게 해결할 수 있는 방법을 사용했다. 10 의 승수 기준으로만 숫자를 처리하는 것이 그 방법이었는데, 4 / 9 등의 숫자가 나오면 별도의 처리가 필요하지만 그 외는 일정하게 처리가 가능한 방식이었다.

 

여기서 Map 은 각 숫자가 어떤 문자로 변환될 지를 저장하기 위해 사용되었다.

 

string intToRoman(int num) {
	map<int, char> m = {{1, 'I'}, {5, 'V'}, ..., {1000, 'M'}};
    string ans = "";
    
    for (int i = 1000; i > 0; i /= 10) {
    	int firstNum = num / i;
        
        if (firstNum == 4) {
        	ans += m[i];
            ans += m[i * 5];
        }
        else if (firstNum == 9) {
        	ans += m[i];
            ans += m[i * 10];
        }
        else {
        	if (firstNum > 4) {
            	ans += m[i * 5];
            }
            for (int k = 0; k < (firstNum % 5); k++) {
            	ans += m[i];
            }
        }
        num %= i;
    }
    return ans;
}

# 13. Roman to Integer

이 문제는 위의 # 12. 문제를 거꾸로 만드는 문제이다.

 

위의 문제는 각 자리수별로 어떤 수가 저장되어있는 지를 확인해서 그 값을 로마자로 바꿔줬는데, 이번 문제는 로마자를 보고 수로 바꿔주었다.

차이점은, 로마자의 경우 현재의 값이 하나의 독립적인 값이 아니라 다음에 나올 값의 전치 값으로 사용될 수 있기 때문에 이를 조건에 추가해서 계산하도록 만들었다.

 

마찬가지로 map 을 사용했는데 이번에는, 각 문자가 어떤 숫자로 변환될 것인지 그 값을 저장하기 위해 사용되었다.

 

int romanToInt(string s) {
	map<char, int> m = {{'M', 1000}, {'D', 500}, ..., {'I', 1}};
    int ans = 0;
    
    for (int i = 0; i < s.length(); i++) {
    	if (m[s[i + 1]] > m[s[i]]) {
        	ans -= m[s[i]];
            ans += m[s[i + 1]];
        }
        else {
        	ans += m[s[i]];
        }
    }
    return ans;
}

'Algorithm > LeetCode' 카테고리의 다른 글

[LeetCode # 17 ~ 19] 문제 풀이  (0) 2021.03.18
[LeetCode # 14 ~ 16] 문제 풀이  (0) 2021.03.09
[LeetCode # 7 ~ 9] 문제 풀이  (0) 2021.02.26
<LeetCode # 4 ~ 6> 문제 풀이  (0) 2021.02.23
<LeetCode # 1 ~ 3> 문제 풀이  (0) 2021.02.21
반응형

# 7. Reverse Integer

한 정수가 주어졌을 때, 자릿수를 뒤집은 정수를 반환하는 문제

ex) 12345 -> 54321

 

이 문제를 보고, 가장 먼저 떠올린 풀이는 정수를 한 자리씩 받아서 문자열로 변환하고, 이를 다시 정수형으로 바꾸는 거였다.

이렇게 하면, 1의 자리부터 문자열의 앞에서 넣으면 자연스럽게 뒤집을 수 있을 것이라 생각했다.

 

여기서 Solution 을 보고 참고했던 점은, 굳이 문자열로 변환했다가 다시 정수로 변환하는 과정을 거칠 필요 없이 새로운 정수에 10을 곱하고 기존 수의 1의 자리수를 새로운 정수의 1의 자리로 만들어주면 되는 점이었다.

그 다음으로 문제의 핵심은 오버플로우가 발생했을 때, 이 값을 어떻게 처리할 것인가. 였다.

 

이건 일일이 확인하는 방법이 가장 확실해 보였다. 새로운 수에 10을 곱하기 전에 조건을 확인하고 진행을 할 수 있도록 했다.

 

#define INT_MAX 2147483647
#define INT_MIN -2147483648

class Solution {
public:
	int reverse(int x) {
		int rev = 0;
		while (x != 0) {
			int pop = x % 10;
			x /= 10;
			if (rev > INT_MAX / 10 || (rev == INT_MAX / 10 && pop > 7)) return 0;
			if (rev < INT_MIN / 10 || (rev == INT_MIN / 10 && pop < -8)) return 0;
			rev = rev * 10 + pop;
		}
		return rev;
	}
};

# 8. String to Integer (atoi)

atoi 함수를 직접 구현하는 문제

 

이 때 문제의 조건은, 숫자 앞의 공백은 무시하고 '+' / '-' 는 결과값의 부호를 의미하는 것이다.

문자열에서 숫자의 조건은, 처음으로 숫자가 나와서 숫자 이외의 문자가 등장하기 전까지이고 숫자가 아닌 문자가 앞에 있는 경우 숫자로 인식하지 않는다.

추가로 오버플로우가 발생할 경우, 오버플로우가 발생하지 않는 최대(최소)값을 반환하도록 한다.

 

이 문제는, 위의 문제 풀이를 참고해서 오버플로우 조건을 검사하도록 하고, 문자열을 숫자로 변환하는 건 앞에서부터 하도록 했다.

 

class Solution {
public:
    int myAtoi(string s) {
        int idx = 0, ret = 0;
        while (s[idx] == ' ') {
            idx++;
        }

        bool negative = false;
        if (s[idx] == '-') {
            negative = true;
            idx++;
        }
        else if (s[idx] == '+')
            idx++;

        while (s[idx] >= '0' && s[idx] <= '9') {
            if (negative && (ret > INT_MAX / 10 || (ret == INT_MAX / 10 && s[idx] >= '8'))) {
                return INT_MIN;
            }
            if (!negative && (ret > INT_MAX / 10 || (ret == INT_MAX / 10 && s[idx] >= '7'))) {
                return INT_MAX;
            }
            ret = ret * 10 + (s[idx] - '0');
            idx++;
        }

        return (negative ? -ret : ret);
    }
};

# 9. Palindrome Number

주어진 정수가 Palindrome Number 인지 확인하는 문제

Palindrome Number 란, 앞뒤 자리가 대칭되는 수 ex) 12345 (X), 12321 (O)

 

처음 생각은, Palindromic String 처럼 가운데 위치를 잡고 그 위치 앞뒤로 대칭인지를 확인하는 방식이었다. 

하지만 정수는 가운데가 어디인지 한 번에 찾는 게 불가능해서 다른 방식을 찾게 되었다.

 

Solution 을 참고해 얻은 결과는, #7 번 문제를 참고해서 숫자를 뒤집는 중간에 자리수가 같거나 한자리 차이나는 순간에 두 수를 비교하면 앞뒤가 대칭인지 알 수 있다는 것이었다.

 

class Solution {
public:
	bool isPalindrome(int x) {
		if (x < 0 || (x != 0 && x % 10 == 0))
			return false;

		int rev = 0;
		while (x > rev) {
			rev = rev * 10 + x % 10;
			x /= 10;
		}

		return rev == x || x == rev / 10;
	}
};

'Algorithm > LeetCode' 카테고리의 다른 글

[LeetCode # 17 ~ 19] 문제 풀이  (0) 2021.03.18
[LeetCode # 14 ~ 16] 문제 풀이  (0) 2021.03.09
[LeetCode # 11 ~ 13] 문제 풀이  (0) 2021.02.28
<LeetCode # 4 ~ 6> 문제 풀이  (0) 2021.02.23
<LeetCode # 1 ~ 3> 문제 풀이  (0) 2021.02.21
반응형

# 4. Median of Two Sorted Arrays

정렬된 두 Array A, B 가 주어졌을 때, 중간값을 구하는 문제

* 수행시간이 O(log(m + n)) 으로 주어짐

 

우선, 내 머리로 이 문제를 풀기엔 역부족이었고 Solution 을 참고했다.

처음 생각했던 것은, 두 Array 가 정렬이 되어 있으니, Merge 를 하면 되지 않을까.. 생각을 했는데 그렇게 되면 O(m + n) 이 되므로 문제 풀이에 적합하지 않았다.

그래서 Solution 을 처음 봤는데 두 Array 를 각각 나눠서 왼쪽 부분과 오른쪽 부분을 각각 합치는데 그렇게 하면 왼쪽의 Max 값과 오른쪽의 Min 값으로 중간값을 구할 수 있다는 것이었다.

근데 여기서, 두 Array 를 나눠서 왼쪽 부분의 Max 가 오른쪽 부분의 Min 보다 작다는 보장을 할 수 있나... 라고 생각을 하던 중 Solution Source Code 를 보고 의문이 풀렸다.

이 문제를 푸는 Solution 의 핵심은 그 위치를 찾는 것이었다. 한 Array 를 기준으로 잡고 그 Array 에서 나눌 위치를 찾는 것을 이분탐색을 통해 수행하는 것, 그것이 이 문제의 해답이었다.

#define _MAX(x, y) ((x) > (y) ? (x) : (y))
#define _MIN(x, y) ((x) < (y) ? (x) : (y))

class Solution {
public:
	double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
		int m = nums1.size(), n = nums2.size();
		if (m > n) {
        	// Array 1 번을 기준으로 잡고 풀이를 진행하기 위해 Array 1 이 짧거나 같도록 유지
		}
        // Slice 위치 탐색을 위한 값 설정
		int iMin = 0, iMax = m, half = (m + n + 1) / 2;
		while (iMin <= iMax) {
        	int i, j; // i 는 Array 1 을 Slice 할 인덱스, j 는 나머지 개수를 채우기 위한 index
			if (i < iMax && nums2[j - 1] > nums1[i]) {
				// Slice 를 했는데 Array 2 의 Left Max 값이 
                // Array 1 의 Right Min 값보다 큰 경우
			}
			else if (i > iMin && nums1[i - 1] > nums2[j]) {
				// Slice 했는데 Array 1 의 Left Max 값이
                // Array 2 의 Right Min 값보다 큰 경우
			}
			else {
            	// 그 외 조건에 맞게 Median 값을 찾기

				if ((m + n) % 2 == 1)
					return (double)maxLeft;

				return (double)(maxLeft + minRight) / 2;
			}
		}
		return 0.0;
	}
};

# 5. Longest Palindromic Substring

String 이 주어지고, 이 String 의 Substring 중에 가장 긴 Palindromic String 을 찾는 문제

* Palindromic String 이란, 좌우가 대칭되는 문자열을 의미한다. ex) "abcba", "StringnirtS", ...

 

가장 먼저 떠올린 생각은, Dynamic Programming 을 활용하면.. 풀 수 있지 않을까 였는데 그 방법이 떠오르지 않아서 중심을 잡고 거기서 확장해 나가는 방법을 떠올렸다.

예를 들어, "ABCDEFG" 라는 문자열이 있으면, 'A', 'B', 'C', .. 각각의 문자를 중심으로 양 옆으로 확장했을 때 좌우 대칭이 이뤄지는 지를 확인하면 되지 않을까 라는 생각이었다.

이렇게 할 경우, Best Case 는 O(n) 의 시간 복잡도를 가질 것이고 Worst 의 경우는 O(n^2) 의 시간 복잡도를 가질 것이라 생각했다. 모든 위치에서 SubString 이 String 길이 만큼의 Palindromic 을 가질 경우가 Worst 라고 생각했지만, 그럴 경우는 거의 없을 것이고 평균적으로 O(n) 에서 크게 벗어나지 않을 것이라는 생각을 했다.

#define _MAX(x, y) ((x) > (y) ? (x) : (y))

int calculate(string &s, int l, int r) {
	while (l >= 0 && r < s.length() && s[l] == s[r]) {
    	// 기준 L, R 을 확인해서 두 문자가 서로 같으면 탐색을 계속한다.
	}
	return r - l - 1;
}

class Solution {
public:
	string longestPalindrome(string s) {
		if (s.length() < 1)
			return "";
		int st = 0, ed = 0;

		for (int i = 0; i < s.length(); i++) {
			int l1, l2; // l1 은 현재 위치 기준 1개부터 시작하고, l2 는 현재 위치 기준 2개씩 시작
			int len = _MAX(l1, l2);
			if (len > ed - st) {
				// 어디부터 시작하고 어디서 끝나는 지 st, ed 저장
			}
		}
		return s.substr(st, ed - st + 1);
	}
};

# 6. ZigZag Conversion

String 과 Row 를 주고, 지그재그로 문자열을 표현했을 때 Row 순으로 저장된 문자열을 찾는 문제

ex) String : "ABCDEFGHIJ", Row : 3
A      E       I

B  D  F  H  J

C      G

Result : "AEIBDFHJCG"

 

이 문제 역시 Solution 을 참고했다.

처음에 문제를 보고, 아! 행별로 문자를 뛰어넘는 Offset 값이 있고, 그걸 구해서 찾으면 되겠다! 라고 생각을 했는데

계산하기에 조건이 조금 귀찮아서 Solution 을 살짝 봤다.

계산하는 방식도 있긴 했지만, 그것보다 더 쉬운 방식이 있어 그걸 참고해서 구현했다.

참고한 방식은, 행별로 문자열을 저장할 공간을 만들어 두고 그냥 문자열을 순서대로 탐색하며 한 문자씩 한 행에 추가하는 방식이었다.

내가 생각한 방식보다 구현도 더 간단하고 시간적인 측면에서도 거의 차이가 없을 것 같아 이 방식을 사용했다.

#define _MIN_(X, Y) ((X) < (Y) ? (X) : (Y))

class Solution {
public:
    string convert(string s, int numRows) {
	if (numRows == 1)
		return s;
	string ret = "";
	vector<string> rows(_MIN_(numRows, s.size())); // 행별로 문자열을 저장해 둘 공간
	int curRow = 0; // 현재 어느 행에 저장할 지
	bool dir = true; // 다음 행의 위치가 어느 방향인지

	for (char c : s) {
        // 문자열 순서대로 탐색하면서 각 문자를 현재 행에 추가
        // 현재 행을 고려해 방향을 정하고, 그 방향으로 행 이동
	}
	for (string tmp : rows) {
        // 마지막에 행 순서대로 문자열 저장
	}
	return ret;
}
};

'Algorithm > LeetCode' 카테고리의 다른 글

[LeetCode # 17 ~ 19] 문제 풀이  (0) 2021.03.18
[LeetCode # 14 ~ 16] 문제 풀이  (0) 2021.03.09
[LeetCode # 11 ~ 13] 문제 풀이  (0) 2021.02.28
[LeetCode # 7 ~ 9] 문제 풀이  (0) 2021.02.26
<LeetCode # 1 ~ 3> 문제 풀이  (0) 2021.02.21
반응형

# 1. Two Sum

Integer Array 와 찾고자 하는 Target 값이 주어졌을 때 Array 에서 더해서 Target 값이 되는 두 수를 찾는 문제

 

문제 풀이는 간단하게, 배열에 있는 모든 값을 Map 에 등록하고 다시 배열을 탐색하면서 현재 위치의 수와 더해서 Target 이 될 수 있는 수가 있는 지 확인하고 존재하면 해당 두 값의 인덱스를 return

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> ans;
        map<int, int> m;
        for (int i = 0; i < nums.size(); i++) {
        	// 배열에 존재하는 모든 값을 map 에 등록 (first = 값, second = 인덱스)
        }

        for (int i = 0; i < nums.size(); i++) {
        	// 다시 배열을 탐색해서 현재 값과 더해서 Target 이 되는 값이 map 에 있는 지 확인
            // 확인해서 값이 존재하면 return
        }
        return ans;
    }
};

# 2. Add Two Numbers

두 리스트의 각각의 원소가 저장하는 값을 더하고, 한 자리만 그 위치에 저장. 10 이상의 값이 나온 경우 다음 계산에서 +1을 해 주는 문제

 

처음 생각은 리스트를 탐색해 두 리스트를 각각 정수형으로 만들고 한 번의 더하기로 합을 만들어 주는 거였는데, 주어진 두 리스트의 길이가 정해진 바가 없어서 정수형으로는 연산이 불가능

그래서 Full Adder 회로와 동일한 동작을 하도록 만들기로 함

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* Head = new ListNode(0);
        ListNode* p, * q, * curr;

        while (p != nullptr || q != nullptr) {
       		// p 와 q 중 어느 하나라도 nullptr 가 아니면 계속 진행
            // 아래 자리에서의 carry 값을 참고해 해당 자리의 sum 을 구하고
            // 그 값을 리스트에 저장
        }

        if (carry > 0)
        	// carry 가 0이 아니면 마지막 자리를 추가
            
        return Head->next;
    }
};

# 3. Longest Substring Without Repeating Characters

주어진 String 에서 동일한 문자가 존재하지 않는 Substring 을 도출하는 문제

 

처음에는 앞에서부터 탐색하며 동일한 문자가 나오지 않을 때까지 SubString 을 이어 나가고 동일한 문자가 나온 경우 앞에서부터 한 자리씩 줄이고 다시 탐색 (Sliding Window 활용)

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        bool check[300] = { false, }; // 동일한 문자가 나왔는 지 확인하기 위한 배열
        int len = s.length();        
        int max = 0, st, ed;
        
        for (st = 0, ed = st + 1; st < len; st++) {
            if (st == 0) {
            	// 처음에는 무조건 SubString 에 추가
            }
            else
            	// 다음부터는 st - 1 위치의 문자를 뺌

            for (; ed < len; ed++) {
            	// 뒤로 늘려가며 같은 문자가 등장하는 지 확인하고
                if (check[s[ed]] == true)
                	// 등장하면 반복 중단
            }
        }

        return max;
    }
};

'Algorithm > LeetCode' 카테고리의 다른 글

[LeetCode # 17 ~ 19] 문제 풀이  (0) 2021.03.18
[LeetCode # 14 ~ 16] 문제 풀이  (0) 2021.03.09
[LeetCode # 11 ~ 13] 문제 풀이  (0) 2021.02.28
[LeetCode # 7 ~ 9] 문제 풀이  (0) 2021.02.26
<LeetCode # 4 ~ 6> 문제 풀이  (0) 2021.02.23
반응형

문제 링크 icpc.me/2508


이 문제는 높이가 서로 다른 나무 N 그루에서 나무를 M 미터만큼 벌목하고자 하는 문제이다.

벌목기를 사용하려고 하는데, 벌목기의 높이 H 를 지정할 수 있고 H 보다 더 큰 나무를 모두 절단해

각 나무의 높이 - H 만큼의 나무를 얻을 수 있다.

이 때 M 미터만큼 벌목을 하기 위해서 H 를 얼마로 설정해야 하는가를 찾는 문제이다.


처음 접근 방식은 N 그루의 나무 중 가장 높이가 큰 나무의 높이로부터 H 를 순차적으로 찾는 방식이었다.

이렇게 할 경우 간단한 문제는 해결이 가능하지만 나무의 개수가 많아질수록 시간이 오래 걸려 시간 초과가 발생하였다.


더보기

// 순차 탐색 


#include <stdio.h>

#include <stdlib.h>



int main() {

int tree, obj;

int *tree_height;

int max_height;

int total;

int prev;

scanf("%d %d", &tree, &obj);

tree_height = calloc(sizeof(int), tree);

for (int i = 0; i < tree; i++) {

scanf("%d", tree_height + i);

}

max_height = tree_height[0];

prev = -1;

while (1) {

total = 0;

for (int i = 0; i < tree; i++) {

if (tree_height[i] - max_height > 0)

total += tree_height[i] - max_height;

}

if (total > obj) {

if (prev < obj && prev != -1)

break;

else max_height++;

}

else if (total < obj) {

if (prev > obj && prev != -1)

break;

else max_height--;

}

else {

break;

}

prev = total;

}

printf("%d\n", max_height);

}


시간 초과가 발생한 후 다음으로 시도한 방법은 나무를 높이 순으로 정렬한 후 순차탐색을 하는 것이었다.

이렇게 할 경우, 마찬가지로 벌목기의 높이 H 를 줄여가며 확인을 해야 하지만 나무가 높이 순으로 정렬되어 있기 때문에

벌목기가 나무보다 높아질 경우 그 뒤의 나무는 자를 수 있는 지를 확인할 필요가 없다.


더보기

// 정렬 후 순차탐색


#include <iostream>

#include <vector>

#include <algorithm>


using namespace std;


bool comp(int a, int b) {

return a > b;

}


int main() {

ios_base::sync_with_stdio(false);

cin.tie(NULL);

cout.tie(NULL);


vector<int> v;

int n, m, input;


cin >> n >> m;

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

cin >> input;

v.push_back(input);

}


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


int max = v[0];

int sum;

while (1) {

sum = 0;

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

if (v[i] <= max)

break;

sum += v[i] - max;

}

if (sum >= m)

break;

max--;

}

cout << max << "\n";

}


두번째 소스 코드 또한 마찬가지로 시간 초과가 발생했다.

아무래도 H 를 바꿔가며 나무를 일일히 확인하는 방법은 시간적으로 많이 소모된다고 생각이 들었다. 그래서 생각해낸 방법이

두번째 방법과 유사하지만 나무를 확인하는 횟수를 줄이는 것이었다.


간단하게 설명하자면, 위의 소스코드는 H 를 변경할 때마다 모든 나무에서 몇 m 씩만큼의 나무를 얻을 수 있는 지를 계산해야 한다.

하지만 이 과정은 나무가 높이 순으로 정렬되어 있기 때문에 불필요한 과정이었다.

이전 단계 (즉, 이전 벌목기의 높이 H) 에서 벌목 가능한 나무를 X 그루라고 하면, 단계가 변할 때마다 총 얻을 수 있는 나무의 양은 X 만큼

증가하게 된다.

추가적으로 H 가 감소했으니, 이전 단계에서 벌목 가능한 나무의 위치 바로 다음 위치부터 있는 나무만을 확인하면 된다.


이렇게 작성한 소스 코드는 다음과 같다.


더보기

// 정렬 후 순차 탐색

// 이전 단계의 탐색 위치를 저장


#include <iostream>

#include <algorithm>

#include <vector>


using namespace std;


bool comp(long long a, long long b) {

return a > b;

}


int main() {

ios_base::sync_with_stdio(false);

cin.tie(NULL);

cout.tie(NULL);


vector<long long> v;

long long n, m, input;


cin >> n >> m;

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

cin >> input;

v.push_back(input);

}


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


long long max = v[0];

long long sum = 0;

long long index = 0;

while (1) {

sum += index; // sum 을 단계 별로 초기화하지 않고 기존의 값에 H 가 변화함에 따라 추가되는 나무를 더해준다.

for (int i = index; i < n; i++) {

if (v[i] <= max) {

index = i;

break;

}

sum += v[i] - max;

}

if (sum >= m)

break;

max--;

}

cout << max << "\n";

}


이렇게 풀었을 때, 더이상 시간초과는 발생하지 않았지만 왠지 모르게 입력되는 값이 커질수록 결과값이 이상하게 출력되었다.

처음에 long long 을 사용하지 않고 int 형을 사용해 자료형 문제로 생각하고 long long 으로 바꾼 후 다시 제출을 했지만

마찬가지로 36% 정도에서 틀렸다는 결과가 발생했다.


그래서 예외 상황이 생길 수 있나 생각했는데 상황을 찾지 못해 결국 풀이 방식을 바꾸는 쪽을 생각했다.


데이터를 정렬하지 않고 문제를 푸는 방법을 생각하게 되었고, 정렬을 하지 않게 되면 이 알고리즘에서 시간을 줄일 수 있는 방법이

H 의 값을 변경하는 횟수를 줄이는 것이었다.


이 알고리즘의 속도는 H 를 얼마나 많이 변경하느냐와 H 에 따라 벌목할 수 있는 나무를 얼마나 탐색하느냐 이 두 가지에 따라 결정된다.

즉, O(H * N) 이 된다고 볼 수 있다. 이 때, N 을 줄이는 것은 앞서 시도한 방법들에서 실패했으니 H 를 줄이는 방법을 생각해 보았다.


H 를 줄이는 방법은 H 를 N 그루의 나무 중 최대 높이부터 1씩 감소시켜 나가는 것이 아니라 최대 높이와 최소 높이를 기준으로

이분 탐색을 하는 방법이었다.


이분 탐색을 함으로써 O(H * N) 은 O(logH * N) 으로 바꿀 수 있게 되고, H 는 최대 1,000,000,000 의 값을 갖는다고 문제의 조건으로

주어졌으니 log(1,000,000,000) ≒ 29 로 알고리즘의 성능은 O(N) 으로 표현할 수 있게 된다.


소스 코드는 다음과 같다.


더보기

// 이분 탐색


#include <iostream>


#define _MAX(A, B) A > B ? A : B


using namespace std;


long long arr[1000001];


int main() {

ios_base::sync_with_stdio(false);

cin.tie(NULL);

cout.tie(NULL);


long long n, m, max = 0;

cin >> n >> m;


for (long long i = 0; i < n; i++) {

cin >> arr[i];

max = _MAX(max, arr[i]);

}


long long low = 0, mid, high = max;

long long sum = 0;


while (low < high) {

sum = 0;

mid = (high + low + 1) / 2;

for (long long i = 0; i < n; i++) {

sum += _MAX(0, arr[i] - mid);

}

if (sum < m) high = mid - 1;

else low = mid;

}


cout << low << "\n";

}


'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1874] 스택 수열  (0) 2021.09.21
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19

+ Recent posts