2471. Minimum Number of Operations to Sort a Binary Tree by Level
내 코드
55ms, 181.1MB
class Solution {
public:
int minimumOperations(TreeNode* root) {
int answer{};
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
vector<pair<int, int>> nodesWithIndex;
int n{};
for (int s{}, e{ static_cast<int>(q.size()) }; s < e; ++s) {
auto now = q.front(); q.pop();
if (now->left) {
q.push(now->left);
nodesWithIndex.emplace_back(now->left->val, n++);
}
if (now->right) {
q.push(now->right);
nodesWithIndex.emplace_back(now->right->val, n++);
}
}
// 최소 교환횟수를 구하는 좋은 방법?
sort(begin(nodesWithIndex), end(nodesWithIndex), [](const pair<int, int>& p1, const pair<int, int>& p2) {
return p1.first < p2.first;
});
vector<bool> visited(n, false);
int swapCnt{};
for (int i{}; i < n; ++i) {
// 이미 정렬된 위치이거나 방문한 노드인 경우 건너뜀
if (visited[i] || nodesWithIndex[i].second == i) continue;
int cycle_size = 0;
int j = i;
// 사이클을 탐색
while (!visited[j]) {
visited[j] = true;
j = nodesWithIndex[j].second;
cycle_size++;
}
// 사이클의 크기가 0보다 클 경우 스왑 수 증가
if (cycle_size > 0) {
swapCnt += (cycle_size - 1);
}
}
answer += swapCnt;
}
return answer;
}
};
최소 교환 횟수 구하기..
오름차순으로 정렬해야 하는 배열에서 최소 정렬 횟수를 구하는 방법은 문제의 정의에 따라 다를 수 있습니다. 여기서는 일반적으로 사용되는 몇 가지 접근 방식을 소개하겠습니다.
1. 인버전 수 (Inversion Count) 이용하기
인버전 수는 배열 내에서 앞에 위치한 원소가 뒤에 있는 원소보다 큰 쌍의 개수를 의미합니다. 인접한 두 원소를 교환하는 방식으로 정렬할 때, 전체 인버전 수이 최소 횟수가 됩니다.
-
계산 방법:
- 병합 정렬(Merge Sort)과 같은 효율적인 정렬 알고리즘을 활용하여 인버전 수를 계산할 수 있습니다. 시간 복잡도는 입니다.
-
적용 예:
def merge_sort(arr): if len(arr) <= 1: return arr, 0 mid = len(arr) // 2 left, inv_left = merge_sort(arr[:mid]) right, inv_right = merge_sort(arr[mid:]) merged, inv_split = merge(left, right) return merged, inv_left + inv_right + inv_split def merge(left, right): result = [] i = j = inv_count = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) inv_count += len(left) - i j += 1 result += left[i:] result += right[j:] return result, inv_count
2. 최장 증가 부분 수열 (Longest Increasing Subsequence, LIS) 이용하기
배열을 정렬하기 위해 최소한으로 이동해야 하는 원소의 수는 전체 원소 수에서 LIS의 길이를 뺀 값입니다.
-
계산 방법:
- 동적 프로그래밍(Dynamic Programming) 또는 이진 탐색(Binary Search)을 활용하여 LIS를 구할 수 있습니다. 시간 복잡도는 입니다.
-
적용 예:
import bisect def length_of_LIS(nums): sub = [] for num in nums: idx = bisect.bisect_left(sub, num) if idx == len(sub): sub.append(num) else: sub[idx] = num return len(sub) array = [10, 9, 2, 5, 3, 7, 101, 18] lis_length = length_of_LIS(array) min_moves = len(array) - lis_length
3. 사이클 기반 최소 스왑 수 (Minimum Swaps Using Cycle Detection)
배열을 정렬하기 위해 필요한 최소 스왑 수는 배열의 사이클 수를 이용해 계산할 수 있습니다.
-
계산 방법:
- 배열의 각 원소를 정렬된 위치와 비교하여 사이클을 형성하고, 각 사이클의 길이에서 1을 뺀 값을 모두 더합니다.
-
적용 예:
def min_swaps(arr): n = len(arr) arr_pos = sorted([(num, i) for i, num in enumerate(arr)], key=lambda it: it[0]) visited = [False] * n swaps = 0 for i in range(n): if visited[i] or arr_pos[i][1] == i: continue cycle_size = 0 j = i while not visited[j]: visited[j] = True j = arr_pos[j][1] cycle_size += 1 if cycle_size > 0: swaps += (cycle_size - 1) return swaps array = [4, 3, 2, 1] print(min_swaps(array)) # 출력: 2
요약
- 인버전 수는 인접 교환을 기준으로 최소 정렬 횟수를 구할 때 유용합니다.
- 최장 증가 부분 수열(LIS) 은 원소 이동을 기준으로 최소 정렬 횟수를 계산할 때 사용됩니다.
- 사이클 기반 스왑 수는 임의의 두 원소를 교환할 수 있을 때 필요한 최소 스왑 수를 계산하는 데 적합합니다.
LIS를 이용하되 C++ 코드로 만들어주세요.
물론입니다! 최장 증가 부분 수열(Longest Increasing Subsequence, LIS)을 이용하여 배열을 오름차순으로 정렬하기 위해 필요한 최소 이동 횟수를 계산하는 C++ 코드를 제공해드리겠습니다. 이 방법은 배열의 전체 원소 수에서 LIS의 길이를 빼면 최소 이동 횟수를 구할 수 있습니다.
아래는 C++로 작성된 예제 코드입니다.
#include <iostream>
#include <vector>
#include <algorithm>
// LIS의 길이를 O(n log n) 시간 복잡도로 계산하는 함수
int length_of_LIS(const std::vector<int>& nums) {
std::vector<int> sub; // LIS를 구성하는 서브 시퀀스를 저장할 벡터
for (const auto& num : nums) {
// 현재 숫자가 서브 시퀀스의 마지막 숫자보다 크면 추가
if (sub.empty() || num > sub.back()) {
sub.push_back(num);
}
else {
// 서브 시퀀스에서 현재 숫자보다 크거나 같은 첫 번째 숫자의 위치를 찾음
auto it = std::lower_bound(sub.begin(), sub.end(), num);
// 해당 위치의 숫자를 현재 숫자로 교체
*it = num;
}
}
return sub.size();
}
int main() {
// 예제 배열
std::vector<int> array = {10, 9, 2, 5, 3, 7, 101, 18};
// LIS의 길이를 계산
int lis_length = length_of_LIS(array);
// 최소 이동 횟수 계산 (전체 원소 수 - LIS의 길이)
int min_moves = array.size() - lis_length;
// 결과 출력
std::cout << "배열: ";
for (const auto& num : array) {
std::cout << num << " ";
}
std::cout << "\nLIS의 길이: " << lis_length;
std::cout << "\n최소 이동 횟수: " << min_moves << std::endl;
return 0;
}
코드 설명
-
length_of_LIS함수:- 이 함수는 주어진 배열
nums에서 최장 증가 부분 수열의 길이를 계산합니다. sub벡터는 현재까지 발견된 LIS를 구성하는 원소들을 저장합니다.- 배열을 순회하면서 현재 숫자가
sub의 마지막 원소보다 크면sub에 추가합니다. - 그렇지 않으면
sub에서 현재 숫자보다 크거나 같은 첫 번째 원소를 찾아 현재 숫자로 교체합니다. 이를 통해sub는 가능한 가장 작은 값들로 구성된 LIS의 길이를 유지하게 됩니다. - 최종적으로
sub.size()가 LIS의 길이가 됩니다.
- 이 함수는 주어진 배열
-
main함수:- 예제 배열을 정의하고
length_of_LIS함수를 호출하여 LIS의 길이를 계산합니다. - 최소 이동 횟수는 배열의 전체 원소 수에서 LIS의 길이를 뺀 값으로 계산됩니다.
- 결과를 출력합니다.
- 예제 배열을 정의하고
실행 예시
위의 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
배열: 10 9 2 5 3 7 101 18
LIS의 길이: 4
최소 이동 횟수: 4
여기서 LIS는 [2, 3, 7, 101]으로, 길이는 4입니다. 따라서 최소 이동 횟수는 전체 원소 수인 8에서 4를 뺀 4가 됩니다.
추가 설명
- 시간 복잡도:
length_of_LIS함수는 각 원소에 대해 이진 탐색을 수행하므로 전체 시간 복잡도는 O(n log n)입니다. - 공간 복잡도: 추가적인 벡터
sub를 사용하므로 공간 복잡도는 O(n)입니다.
근데 [7, 6, 5, 4]에서 LIS는 1인데 최소 교환 횟수는 2아닌가요? [7,4], [6,5] 교환하면 정렬되는데요?
맞습니다. 사용자가 제시한 [7, 6, 5, 4] 예제에서 **최장 증가 부분 수열 (LIS)**을 이용한 방법과 사이클 기반 최소 스왑 수를 이용한 방법이 서로 다른 결과를 도출하는 이유는 두 방법이 서로 다른 정렬 연산을 기준으로 하기 때문입니다.
정리하자면:
-
LIS를 이용한 방법 (최소 이동 횟수):
배열을 정렬하기 위해 원소를 이동시키는 최소 횟수를 계산합니다. 이 경우,최소 이동 횟수 = 배열의 크기 - LIS의 길이로 계산합니다.
예제[7, 6, 5, 4]에서는 LIS의 길이가 1이므로, 최소 이동 횟수는4 - 1 = 3입니다. 이는 원소를 이동시켜 정렬할 때 필요한 최소 횟수입니다. -
사이클 기반 최소 스왑 수:
배열을 정렬하기 위해 임의의 두 원소를 교환하는 최소 횟수를 계산합니다. 이 경우, 사이클의 수를 이용하여 계산합니다.
같은 예제[7, 6, 5, 4]에서는 2번의 스왑으로 정렬할 수 있습니다:- 첫 번째 스왑: 7과 4를 교환하여
[4, 6, 5, 7] - 두 번째 스왑: 6과 5를 교환하여
[4, 5, 6, 7]
- 첫 번째 스왑: 7과 4를 교환하여
따라서, 두 방법은 정렬을 수행하는 연산의 종류에 따라 다른 결과를 도출하게 됩니다. 사용자가 원래 의도한 최소 스왑 횟수를 구하고자 한다면, 사이클 기반 최소 스왑 수 방법을 사용해야 합니다.
사이클 기반 최소 스왑 수를 구하는 C++ 코드 제공
아래는 사이클 기반 최소 스왑 수를 구하는 C++ 코드입니다. 이 코드는 배열을 오름차순으로 정렬하기 위해 필요한 최소 스왑 횟수를 계산합니다.
#include <iostream>
#include <vector>
#include <algorithm>
// 최소 스왑 수를 계산하는 함수
int min_swaps(std::vector<int>& arr) {
int n = arr.size();
// 원소와 원래 인덱스를 저장하는 벡터
std::vector<std::pair<int, int>> arr_pos(n);
for (int i = 0; i < n; ++i) {
arr_pos[i] = {arr[i], i};
}
// 원소를 기준으로 정렬
std::sort(arr_pos.begin(), arr_pos.end(),
[&](const std::pair<int, int>& a, const std::pair<int, int>& b) -> bool {
return a.first < b.first;
});
// 방문 여부를 표시하는 벡터
std::vector<bool> visited(n, false);
int swaps = 0;
for (int i = 0; i < n; ++i) {
// 이미 정렬된 위치이거나 방문한 노드인 경우 건너뜀
if (visited[i] || arr_pos[i].second == i)
continue;
int cycle_size = 0;
int j = i;
// 사이클을 탐색
while (!visited[j]) {
visited[j] = true;
j = arr_pos[j].second;
cycle_size++;
}
// 사이클의 크기가 0보다 클 경우 스왑 수 증가
if (cycle_size > 0) {
swaps += (cycle_size - 1);
}
}
return swaps;
}
int main() {
// 예제 배열
std::vector<int> array1 = {10, 9, 2, 5, 3, 7, 101, 18};
std::vector<int> array2 = {7, 6, 5, 4};
// 배열을 정렬하기 위한 최소 스왑 수 계산
int swaps1 = min_swaps(array1);
int swaps2 = min_swaps(array2);
// 결과 출력
std::cout << "배열1: ";
for (const auto& num : array1) {
std::cout << num << " ";
}
std::cout << "\n배열1을 정렬하기 위한 최소 스왑 수: " << swaps1 << std::endl;
std::cout << "\n배열2: ";
for (const auto& num : array2) {
std::cout << num << " ";
}
std::cout << "\n배열2를 정렬하기 위한 최소 스왑 수: " << swaps2 << std::endl;
return 0;
}
코드 설명
-
min_swaps함수:- 원소와 원래 인덱스를 저장:
arr_pos벡터에 배열의 원소와 그 원소의 원래 인덱스를 저장합니다. - 정렬:
원소의 값을 기준으로arr_pos를 정렬합니다. 이렇게 하면 각 원소가 최종적으로 위치해야 할 인덱스를 알 수 있습니다. - 사이클 탐색:
각 원소가 자신의 정렬된 위치로 가기 위해 필요한 스왑을 계산하기 위해 사이클을 탐색합니다. 사이클의 크기가k라면,k-1번의 스왑이 필요합니다. - 스왑 수 계산:
모든 사이클에 대해 필요한 스왑 수를 합산하여 최소 스왑 수를 반환합니다.
- 원소와 원래 인덱스를 저장:
-
main함수:- 두 가지 예제 배열을 정의하고, 각각에 대해
min_swaps함수를 호출하여 최소 스왑 수를 계산합니다. - 결과를 출력합니다.
- 두 가지 예제 배열을 정의하고, 각각에 대해
실행 예시
위의 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다:
배열1: 10 9 2 5 3 7 101 18
배열1을 정렬하기 위한 최소 스왑 수: 4
배열2: 7 6 5 4
배열2를 정렬하기 위한 최소 스왑 수: 2
추가 설명
-
시간 복잡도:
- 정렬:
- 사이클 탐색: 따라서 전체 시간 복잡도는 입니다.
-
공간 복잡도:
추가적으로arr_pos와visited벡터를 사용하므로 의 공간 복잡도를 가집니다.
결론
-
LIS 기반 방법:
- 용도: 원소를 이동시켜 배열을 정렬할 때 필요한 최소 이동 횟수 계산
- 계산법:
최소 이동 횟수 = 배열의 크기 - LIS의 길이 - 예시:
[7, 6, 5, 4]에서 최소 이동 횟수는 3
-
사이클 기반 최소 스왑 수:
- 용도: 임의의 두 원소를 교환하여 배열을 정렬할 때 필요한 최소 스왑 횟수 계산
- 계산법: 배열의 사이클을 탐색하여 각 사이클의 크기에서 1을 뺀 값을 합산
- 예시:
[7, 6, 5, 4]에서 최소 스왑 횟수는 2
Solution
Approach 1: Hash Map
- 191ms, 212.67MB
- Complexity
- Let be the total number of nodes in the binary tree.
- Time Complexity:
- Space Complexity:
class Solution {
public:
int minimumOperations(TreeNode* root) {
queue<TreeNode*> queue;
queue.push(root);
int totalSwaps = 0;
// Process tree level by level using BFS
while (!queue.empty()) {
int levelSize = queue.size();
vector<int> levelValues(levelSize);
// Store values of current level and add children to queue
for (int i = 0; i < levelSize; i++) {
TreeNode* node = queue.front();
queue.pop();
levelValues[i] = node->val;
if (node->left != nullptr) queue.push(node->left);
if (node->right != nullptr) queue.push(node->right);
}
// Add minimum swaps needed for current level
totalSwaps += getMinSwaps(levelValues);
}
return totalSwaps;
}
private:
// Calculate minimum swaps needed to sort an array
int getMinSwaps(vector<int>& original) {
int swaps = 0;
vector<int> target = original;
sort(target.begin(), target.end());
// Map to track current positions of values
unordered_map<int, int> pos;
for (int i = 0; i < original.size(); i++) {
pos[original[i]] = i;
}
// For each position, swap until correct value is placed
for (int i = 0; i < original.size(); i++) {
if (original[i] != target[i]) {
swaps++;
// Update position of swapped values
int curPos = pos[target[i]];
pos[original[i]] = curPos;
swap(original[curPos], original[i]);
}
}
return swaps;
}
};
GPT
아래 코드는 이진 트리의 각 레벨(층)마다 노드의 값들을 정렬하기 위해 필요한 최소 스왑(swap) 횟수를 합산한 결과를 구하는 알고리즘입니다. 즉, 트리의 레벨별로 주어진 노드 값들을 정렬할 때, 전체 스왑 횟수의 합을 계산합니다.
전체 로직
-
BFS(너비 우선 탐색) 로 트리를 레벨 단위로 순회
- 각 레벨의 노드들을 큐에서 꺼내어, 그 노드들의 값을
levelValues에 저장합니다. - 이때, 자식 노드(왼쪽·오른쪽)가 있으면 큐에 추가해서, 다음 레벨을 차례로 방문하게 합니다.
- 각 레벨의 노드들을 큐에서 꺼내어, 그 노드들의 값을
-
레벨 단위로 최소 스왑 계산
levelValues를 정렬하기 위해 필요한 최소 스왑 횟수를 구해totalSwaps에 더해나갑니다.- 모든 레벨을 마치면,
totalSwaps가 최종 결과가 됩니다.
코드 구조
int minimumOperations(TreeNode* root) {
queue<TreeNode*> queue;
queue.push(root);
int totalSwaps = 0;
// BFS로 트리 레벨 단위 접근
while (!queue.empty()) {
int levelSize = queue.size();
vector<int> levelValues(levelSize);
// 현재 레벨의 노드들을 꺼내며 값 저장
for (int i = 0; i < levelSize; i++) {
TreeNode* node = queue.front();
queue.pop();
levelValues[i] = node->val;
if (node->left != nullptr) queue.push(node->left);
if (node->right != nullptr) queue.push(node->right);
}
// 현재 레벨의 노드 값들을 정렬하는 데 필요한 스왑 횟수 계산
totalSwaps += getMinSwaps(levelValues);
}
return totalSwaps;
}
queue에는 각 레벨의 노드들이 순차적으로 들어갑니다.- 한 레벨을 처리할 때마다
levelSize만큼 pop하여 해당 레벨의 모든 노드를 방문·처리한 뒤, 그 노드들의 자식(다음 레벨 노드들)은 push합니다. - 이렇게 BFS가 끝날 때까지 반복하면, 전체 트리를 레벨 단위로 처리한 것이 됩니다.
레벨별 최소 스왑 횟수: getMinSwaps
int getMinSwaps(vector<int>& original) {
int swaps = 0;
vector<int> target = original;
sort(target.begin(), target.end());
// 각 값이 현재 어떤 인덱스에 있는지 기록
unordered_map<int, int> pos;
for (int i = 0; i < original.size(); i++) {
pos[original[i]] = i;
}
// 순서대로 target[i]가 오도록 swap
for (int i = 0; i < original.size(); i++) {
if (original[i] != target[i]) {
swaps++;
// target[i]값이 들어갈 위치(curPos)를 찾아 swap
int curPos = pos[target[i]];
pos[original[i]] = curPos;
std::swap(original[curPos], original[i]);
}
}
return swaps;
}
- 정렬된 상태(
target) 와 현재 상태(original)를 비교 - 현재 배열
original을 한 칸씩 확인(i증가)하면서,- 만약
original[i]가target[i]가 아니라면,target[i]가 있는 위치(curPos)를 찾는다. (pos[target[i]])original[i]와original[curPos]를 swap하여,target[i]를 제자리에 둔다.- swap 횟수
swaps++ pos맵을 갱신 (이전에original[i]가 있던 곳에target[i]가 들어갔으므로)
- 만약
- 모든 원소가
target과 동일해질 때까지 진행 → 그 과정에서 발생한 swap 횟수가 정렬에 필요한 최소 횟수가 됩니다.
왜 최소 스왑?
- 배열을 정렬할 때, 사이클 분해(cycle decomposition) 를 이용하면, 각 사이클에 필요한 스왑 횟수 = (사이클 길이 - 1)임이 알려져 있습니다.
- 위 로직은
pos맵을 사용해 직접 해당 사이클을 추적·교환하는 방법으로, 결과적으로 최소 swap 횟수를 구합니다.
핵심 요약
- 트리의 레벨 별로 노드 값을 추출 (
levelValues). - 각 레벨에서
getMinSwaps함수를 이용해 그 레벨의 노드들을 정렬하는 데 필요한 스왑 횟수를 계산. - 모든 레벨에 대해 구한 스왑 횟수를 합산한 값이 최종 결과.
즉, 한 층씩 내려가며 해당 층의 노드들을 단독으로 정렬한다고 가정했을 때, 각 층마다 필요한 스왑 횟수의 합을 구하는 문제인 것입니다.
Approach 2: Bit Manipulation
- 257ms, 321.31MB
- Complexity
- Let be the total number of nodes in the binary tree.
- Time Complexity:
- Space Complexity:
class Solution {
// Constants for bit manipulation
const int SHIFT = 20;
const int MASK = 0xFFFFF;
public:
int minimumOperations(TreeNode* root) {
queue<TreeNode*> queue;
queue.push(root);
int swaps = 0;
// Process tree level by level using BFS
while (!queue.empty()) {
int levelSize = queue.size();
vector<long long> nodes(levelSize);
// Store node values with encoded positions
for (int i = 0; i < levelSize; i++) {
TreeNode* node = queue.front();
queue.pop();
// Encode value and index: high 20 bits = value, low 20 bits =
// index
nodes[i] = (static_cast<long long>(node->val) << SHIFT) + i;
if (node->left != nullptr) queue.push(node->left);
if (node->right != nullptr) queue.push(node->right);
}
// Sort nodes by their values (high 20 bits)
sort(nodes.begin(), nodes.end());
// Count swaps needed to match indices with original positions
for (int i = 0; i < levelSize; i++) {
int origPos = static_cast<int>(nodes[i] & MASK);
if (origPos != i) {
// Swap nodes and decrement i to recheck current position
swap(nodes[i], nodes[origPos]);
swaps++;
i--;
}
}
}
return swaps;
}
};
GPT
이 코드는 이진 트리(TreeNode* root)에서 각 레벨(층)에 존재하는 노드들의 값을 오름차순으로 정렬 하기 위해 필요한 스왑(swap) 횟수를 모두 합산하는 로직입니다.
기존 구현과 달리, 노드의 값과 해당 레벨 내에서의 인덱스를 하나의 64비트 정수(long long)에 비트로 인코딩하여 저장하고, 정렬 후 다시 인덱스를 맞춰 주는 방식으로 스왑 횟수를 계산하고 있습니다.
주요 아이디어
-
레벨별(BFS) 접근
- BFS(너비 우선 탐색)로 트리를 한 레벨씩 처리합니다.
queue를 사용하여, 현재 레벨의 모든 노드를 꺼낸 후, 그 자식 노드를 다음 레벨 큐에 넣는 방식으로 진행합니다.
-
노드의
(값, 인덱스)를 하나의 수로 인코딩- 각 레벨에서
levelSize만큼 노드를 뽑아, 이 노드들이 “해당 레벨”에서 인덱스 0부터 까지 순서대로 들어왔다고 칩시다. nodes[i]에 다음을 저장:- 여기서
SHIFT는 20,MASK가 0xFFFFF(20비트) - 즉, 상위 20비트는 노드의 값(
node->val), 하위 20비트는 “레벨 내 인덱스(i)”입니다. - 이렇게 하면 한 변수 안에 (값, 인덱스)를 함께 담을 수 있습니다.
- 여기서
- 각 레벨에서
-
정렬: 노드 값 기준 (상위 20비트) 오름차순
sort(nodes.begin(), nodes.end())를 하면, C++ STL 정렬은 기본적으로 원소의 숫자 크기를 기준으로 정렬합니다.- 이때,
node->val << SHIFT가 숫자 크기에 결정적 영향을 주므로, 값이 작은 순으로 오름차순이 됩니다. - 정렬 후,
nodes[i]가 “원래는 i번째 위치(인덱스)에 있어야 할 노드”라는 의미입니다(값 기준으로 정렬된 상태).
-
스왑 계산: 최소 스왑으로 인덱스를 맞추기
- 정렬을 마친 뒤,
nodes[i]가 실제로 레벨 내 인덱스 i에 있어야 합니다. - 하지만,
nodes[i]의 하위 20비트(origPos)를 보면, 이 노드가 원래 몇 번째 인덱스에서 왔는지 알 수 있습니다. - 만약
origPos != i라면, “현재 i 위치에는origPos에서 온 노드가 잘못 자리해 있다”는 뜻이므로, 스왑을 통해 맞춰 줍니다.- 스왑 후, 다시
i위치를 재확인하기 위해i--로 한 칸 뒤로 되돌려(루프에서 i++가 곧 진행되므로, 결국 같은 위치를 다시 체크), 교정된 위치가 올바른지 또 확인하는 방식입니다. - 이 로직은 사이클(cycle)을 풀어 정렬할 때 자주 쓰는 기법으로, 결과적으로 최소 스왑 횟수를 계산하게 됩니다.
- 스왑 후, 다시
- 정렬을 마친 뒤,
코드 흐름 요약
int minimumOperations(TreeNode* root) {
const int SHIFT = 20; // 상위 20비트로 값 저장
const int MASK = 0xFFFFF; // 하위 20비트(=원래 인덱스) 추출용
queue<TreeNode*> queue;
queue.push(root);
int swaps = 0;
while (!queue.empty()) {
int levelSize = queue.size();
vector<long long> nodes(levelSize);
// (A) 현재 레벨 노드들을 꺼내서, (값<<SHIFT + i)로 저장
for (int i = 0; i < levelSize; i++) {
TreeNode* node = queue.front();
queue.pop();
long long encoded = (static_cast<long long>(node->val) << SHIFT) + i;
nodes[i] = encoded;
// 다음 레벨 노드 준비
if (node->left) queue.push(node->left);
if (node->right) queue.push(node->right);
}
// (B) 값(상위 20비트) 기준 오름차순 정렬
// 즉, node->val이 작은 순서
sort(nodes.begin(), nodes.end());
// (C) 정렬된 상태에서, 인덱스 i에 있어야 할 원소가
// 실제로 i의 위치가 맞는지 확인 & 스왑
for (int i = 0; i < levelSize; i++) {
int origPos = static_cast<int>(nodes[i] & MASK);
if (origPos != i) {
// 인덱스 origPos와 swap
swap(nodes[i], nodes[origPos]);
swaps++;
i--; // 이 위치를 다시 검사
}
}
}
return swaps;
}
복잡도 & 특이사항
-
시간 복잡도
- 트리의 총 노드가 개라고 할 때, BFS로 레벨마다 노드를 모아서 정렬합니다.
- 최악의 경우(한 레벨이 매우 많거나, 예를 들어 완전 이진 트리에서 말단 레벨이 N/2 정도 차지) ⇒ 정렬에 가 걸릴 텐데, 각 레벨 합이 결국 전체 .
- 결과적으로 전체 트리를 한 번 순회 + 정렬이 합쳐져 정도가 되며, 여기서 은 최대 레벨 크기( 보통 ), 실제로는 대략 안에 해결 가능합니다.
-
비트 인코딩은 “하나의 64비트에 (값, 인덱스)를 담아 정렬 시 편의”를 위해 쓴 트릭입니다.
- 주의: 노드의 값(
node->val)이 20비트를 초과하는 경우(즉, 이상)라면, 상위 20비트에 인덱스를 넣을 수 없어 충돌이 일어날 수 있습니다. - 문제에서 허용되는 값 범위에 따라 이 트릭을 쓸 수 있습니다. 만약 값이 매우 클 수 있다면, 별도로
pair<int, int>구조체 정렬을 사용하는 편이 안전합니다.
- 주의: 노드의 값(
결론
- 이진 트리를 레벨별로 나누어, 각 레벨을 값의 오름차순으로 “정렬해야 할 때” 필요한 스왑 횟수를 구하는 문제입니다.
- 레벨마다 노드의 값·인덱스를 기록하고, 값 기준으로 정렬한 뒤, 인덱스를 맞추는 데 필요한 스왑 횟수를 누적합니다.
- 비트 연산(
SHIFT,MASK)을 사용해 한long long변수에 (값, 인덱스)를 인코딩하여 정렬과 원래 인덱스 추적을 동시에 처리하고 있습니다.