Skip to content

[Flynn] Week 08 #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions clone-graph/flynn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* 풀이
* - BFS와 해시맵을 사용하여 풀이합니다
*
* Big O
* - N: 주어진 노드의 개수
* - E: 주어진 노드의 간선의 개수
*
* - Time complexity: O(E)
* - 한 Node에서 다른 Node로 향하는 모든 edge를 두번씩 탐색합니다 (두 방향으로 연결되어 있기 때문)
* - Space complexity: O(N)
* - 해시맵에 최대 N개의 노드를 저장합니다
*/

/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/

class Solution {
public:
Node* cloneGraph(Node* node) {
if (node == nullptr) return nullptr;

unordered_map<Node*, Node*> node_map;
node_map[node] = new Node(node->val);

queue<Node*> q;
q.push(node);

while (!q.empty()) {
Node* p = q.front();
q.pop();

for (Node* neighbor : p->neighbors) {
// 방문한 적이 없는 노드일 경우
if (node_map.find(neighbor) == node_map.end()) {
// node_map에 새로운 노드를 복제하여 추가
node_map[neighbor] = new Node(neighbor->val);

// 큐에 추가
q.push(neighbor);
}

node_map[p]->neighbors.push_back(node_map[neighbor]);
}
}

return node_map[node];
}
};
77 changes: 77 additions & 0 deletions longest-common-subsequence/flynn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 풀이 1
* - 2차원 DP를 사용하여 풀이합니다
* DP[i][j]: text1의 i번째 문자까지와 text2의 j번째 문자까지 비교했을 때, 가장 긴 공통 부분 문자열의 길이
* 즉, text1[0 .. i - 1]와 text2[0 .. j - 1]의 가장 긴 공통 부분 문자열의 길이
* DP[i][j] = if text1[i - 1] == text2[j - 1] then DP[i - 1][j - 1] + 1
* else max(DP[i - 1][j], DP[i][j - 1])
* - 풀이 2로 공간복잡도를 줄일 수 있습니다
*
* Big O
* - M: text1의 길이
* - N: text2의 길이
*
* - Time complexity: O(N * M)
* - Space complexity: O(N * M)
*/

class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
size_t m = text1.size();
size_t n = text2.size();

vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
}
}

return dp[m][n];
}
};

/**
* 풀이 2
* - 풀이 1의 DP 전개 과정을 보면 우리한테는 DP 배열 두 행만 필요하다는 걸 알 수 있습니다
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 unique-paths 문제처럼 최적화를 할 수 있었군요.. 잘 봤습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다 대부분의 2D 배열을 이용하는 DP 풀이는 이런 식의 공간 복잡도 최적화가 가능합니다 ㅎㅎ

*
* Big O
* - M: text1의 길이
* - N: text2의 길이
*
* - M >= N이 되도록 고릅니다
*
* - Time complexity: O(N * M)
* - Space complexity: O(N)
*/

class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
size_t m = text1.size();
size_t n = text2.size();

if (m < n) return longestCommonSubsequence(text2, text1);

vector<int> dp1(n + 1, 0);
vector<int> dp2(n + 1, 0);

for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (text1[i - 1] == text2[j - 1]) dp2[j] = dp1[j - 1] + 1;
else dp2[j] = max(dp1[j], dp2[j - 1]);
}

if (i == m) break;

dp1.swap(dp2);
dp2.clear();
dp2.resize(n + 1, 0);
}

return dp2[n];
}
};
73 changes: 73 additions & 0 deletions longest-repeating-character-replacement/flynn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* 풀이
* - 주어진 s로 만들 수 있는 가장 긴 valid substring의 길이를 찾는 문제입니다
* - valid substring: 최대 k개의 문자를 바꿔서, 모든 문자가 같게 만들 수 있는 substring
*
* - 두 단계로 나누어 풀이에 대해 생각할 수 있습니다
*
* - 1. 특정 길이의 valid substring을 만들 수 있는지 확인
* - 함수 bool can_make_valid_substring(string const s, int substr_length, int k)
* - 특정 길이의 substring에 대해서, 등장 빈도가 가장 높은 문자의 빈도수를 저장합니다 (max_freq)
* 만약 해당 substring이 valid substring이라면, max_freq + k >= substr_length 이어야 합니다
*
* - 2. 최대 길이의 valid substring을 찾는다
* - 이진탐색을 통해 최대 길이를 찾는다
* - 함수 int characterReplacement(string s, int k)
* - 주어진 문자열 s로 만들 수 있는 substring의 길이는 1이상 s.size() 이하입니다
* 이진 탐색의 범위를 위에서 언급한 대로 설정하고, 현재 탐색하려는 길이 (mid)에 대해서
* can_make_valid_substring 함수를 호출하여 현재 길이로 valid substring을 만들 수 있는지 확인합니다
* 이진 탐색 알고리즘의 전개 및 결과에 대한 설명은 https://github.com/DaleStudy/leetcode-study/discussions/332를 참고해주세요 :)
*
* Big O
* - N: 주어진 문자열 s의 길이
* - K: 주어진 정수 k
*
* - Time complexity: O(N * logN)
* - Space complexity: O(1)
*/

class Solution {
public:
bool can_make_valid_substring(string const s, int substr_length, int k) {
// 문자의 빈도수를 저장하는 배열입니다
array<int, 26> freq;
freq.fill(0);

// 최대 빈도수를 저장하는 변수입니다
int max_freq = 0;

int window_start = 0;

for (int window_end = 0; window_end < s.size(); ++window_end) {
++freq[s[window_end] - 'A'];

int curr_size = window_end - window_start + 1;
if (curr_size > substr_length) {
--freq[s[window_start] - 'A'];
++window_start;
}

max_freq = max(max_freq, freq[s[window_end] - 'A']);
if (max_freq + k >= substr_length) return true;
}

return false;
}

int characterReplacement(string s, int k) {
int lo = 1;
int hi = s.size() + 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;

if (can_make_valid_substring(s, mid, k)) lo = mid + 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바이너리 서치로 푸시다니 엄청 신선하네요
end 포인터를 한 칸씩 옮기면서 start 포인터를 움직이는 방식으로 풀었는데, 여기서는 "만들 수 있는 부분 문자열의 길이를 lo로 갱신" 하는 것이 핵심인가 보네요 ㅎㅎ 잘 봤습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 아이디어가 안 떠올라서 리트코드의 에디토리얼을 참고했는데, 신선하더라구요 ㅎㅎ 감사합니다
이 풀이보다 시간복잡도 면에서 더 효율적인 풀이가 있긴 하지만요

else hi = mid;
}

// 이진탐색이 종료되면 lo는 최대 길이보다 1 큰 값이 된다.
// EX: hi lo
// T T T T F F F F
// 따라서 최대 길이는 lo - 1이 된다
return lo - 1;
}
};
48 changes: 48 additions & 0 deletions merge-two-sorted-lists/flynn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 풀이
* - 주어진 두 링크드리스트의 각 node를 비교하며 반환할 새 링크드리스트에 추가해줍니다
*
* Big O
* - N: 주어진 두 링크드리스트 list1, list2의 노드 개수의 총합
*
* - Time complexity: O(N)
* - Space complexity: O(1)
* - 반환하는 링크드리스트를 복잡도에 포함시키지 않을 시, 공간복잡도는 N에 상관 없이 일정합니다
*/

/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* head = new ListNode();
ListNode* node = head;

ListNode* p = list1;
ListNode* q = list2;

while (p != nullptr && q != nullptr) {
if (p->val < q->val) {
node->next = p;
p = p->next;
} else {
node->next = q;
q = q->next;
}
node = node->next;
}

if (p != nullptr) node->next = p;
if (q != nullptr) node->next = q;

return head->next;
}
};
54 changes: 54 additions & 0 deletions sum-of-two-integers/flynn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* 풀이
* - 두 정수를 한 bit씩 더하는 방식으로 풀이합니다
* - 두 정수에 대해 이진 덧셈을 진행할 때, 해당 자리수의 bit 두 개와 carry를 비교하여 새로운 carry와 해당 자리수의 덧셈 결과를 얻을 수 있습니다 -> adder 함수 참고
* - 각 비트에 대해 adder 함수를 호출하여 덧셈을 진행합니다
* - res의 특정 자리에 덧셈 결과를 넣어주는 것이 까다로웠는데, position이라는 일종의 bitmask를 사용하여 해결할 수 있었습니다
* - 저는 Nand2Tetris 라는 책/강의를 보면서 이 전에 bitwise 산술 연산기를 구현한 적이 있었는데, 그 경험이 큰 도움이 되었습니다
* 궁금하신 분들께 coursera 강의 링크를 첨부합니다 (무료) (https://www.coursera.org/learn/build-a-computer) (2강에 나옴)
*
* Big O
* - N: a와 b 중 큰 수의 비트 수 <= 32 (c++ 기준)
*
* - Time complexity: O(N <= 32) = O(1)
* - Space complexity: O(1)
*/

class Solution {
public:
// returns {carry, result}
// carry와 result를 아래와 같은 bool 연산으로 표현할 수 있다는 사실은
// x, y, c에 대하여 벤 다이어그램을 그려보면 쉽게 파악할 수 있습니다
pair<bool, bool> adder(bool x, bool y, bool c) {
return {(x & y) | (x & c) | (y & c), x ^ y ^ c};
}

int getSum(int a, int b) {
bool carry = 0;
unsigned int res = 0;
unsigned int position = 1;

// 32 비트 정수 범위 내에서 덧셈을 진행합니다
// 32 비트 모두 덧셈을 진행했거나, 더 더할 비트가 없다면 루프를 종료합니다
while (position && (a || b || carry)) {
bool lsb_a = a & 1;
a >>= 1;

bool lsb_b = b & 1;
b >>= 1;

auto [new_carry, new_res] = adder(lsb_a, lsb_b, carry);

carry = new_carry;
if (new_res) res |= position;

// position이 unsigned int (32비트)이므로
// bitwise left shift 연산을 32번 수행하면 0이 됨
// 1000 0000 0000 0000 0000 0000 0000 0000 => 0000 0000 0000 0000 0000 0000 0000 0000
// position이 0이 되면 32비트 모두 덧셈을 완료했다는 뜻이므로 loop를 종료함
position <<= 1;
}

return (int) res;
}
};