핵심은 left와 right가 같다면, 그 안쪽도 같은지 계속 들어가는 것이다. 1. left right index가 같은 경우 palindromic 이므로 초기값 true 2. 가능한 모든 substring을 최소 단위부터 탐색한다. 3. left와 right의 index의 글자가 같은 경우 left 다음 right 이전의 문자도 동일한지 확인
ex : bab 라면 a는 1. 에서 palindromic 확인했으므로 bb 일 때 이전과 같은지 확인
ex : baab 라면 aa는 2. 에서 palindromic 확인했으므로 bb 일 때 이전과 같은지 확인 4. palindromic 인 경우 가장 긴 substring의 길이와 start 위치 저장
표를 자세히 보면 true 인 곳을 중심으로, 대각선 오른쪽 위에 정답이 있음을 확인할 수 있다.
가장 먼저 떠오른 아이디어는 긴 문자열의 기준으로 작은 문자열의 문자와 비교하며 길이를 구하는 방법이다.
하지만 이 방법은 O(n^2) 이므로 O(n)인 방법을 생각해보았다.
string관련 무네를 풀 때는, map/ dp 선에서 다 풀리기에 map으로 풀어보았다.
1. text1 과 text2의 lettermap을 만든다 2. lettermap을 비교하며 둘 중에 작은 값을 누적시킨다.ex) text1_map[‘a’] = 3 , text2_map[‘a’] = 1 이면 sum에 1을 누적 3. sum 리턴 실수 : 내가 문제를 제대로 이해하지 못했다. text1 이 abc , text2 가 cba 라면 리턴값은 3이 아니라, 1이 되어야 한다. 즉 문자의 개수만 같아서 되는 것이 아니라, 문자의 순서도 같아야 한다.
하지만 내 풀이는 substring의 순서를 어겼다.
text1 : abc , text2 : cba 라면 return 값은 3이 아니라 1이 되어야 한다.
정답 풀이 / 해설 aab와 aca 비교한다 했을 때, 1) aab ac 와 2) aa aca 일 때 각각의 개수와 둘 중의 큰 값을 채택 1)aab , a 와 aa , ac 그리고 2)aa , ac 와 a , aca 일 때 각각의 개수와 둘 중의 큰 값을 채택 …. 이런 식으로 dp를 쌓아 올린다. 즉 처음 떠오르는 접근법대로 하면 된다.
Sourcecode
## 내 풀이
class Solution {
public:
int longestCommonSubsequence(string text1, string text2)
{
if(text1.size() == 0 || text2.size() == 0) return 0;
int sum = 0;
vector<int> map1(26,0);
vector<int> map2(26,0);
for(int i = 0; i<text1.size();i++)
map1[text1[i] - 'a'] += 1;
for(int i = 0; i<text2.size();i++)
map2[text2[i] - 'a'] += 1;
for(int i = 0; i<26; i++)
sum += min(map1[i],map2[i]);
return sum;
}
};
## 정담 풀이 및 출처
중간에 있는 for문은 다음의 표를 보면 이해하기 쉽다.
(0)
a (0)
c (0)
e (0)
a (0)
1
1
1
b (0)
1
1
1
c (0)
1
2
2
d (0)
1
2
2
e (0)
1
2
3
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size();
int m = text2.size();
if(n == 0 || m == 0)
return 0;
vector<vector<int>> dp(n+1, vector<int>(m+1, 0)); //2차원 dp vector선언
// 아무것도 없는 것과 비교하는 것은 0이므로 초기값을 0으로 설정
for(int i=1; i<=n; i++)
{
for(int j=1; j<=m; j++)
{
if(text1[i-1] == text2[j-1])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[n][m];
}
};
New things learned
dp의 목적은 가능한 모든 경우를 파악해 결과를 도출하는 것이다. 이전의 결과들의 조금의 변화가 생겨서 다음의 결과가 가능하면해당하는 모든 이전의 값들을 사용해 다음 dp를 구한다. 위 예시에서 aab와 aca를 비교하려면 aab, ac 와 aa, aca의 값을 이용하는 것 처럼 말이다.
비교하는 단어는 중복을 방지하기 위해 해당 단어의 index를 flag를 세워 탐색을 건너뛴다.
Sourcecode
## 직접 작성한 코드
짧은 example에서는 제대로 작동하지만, Time Limit Exceeded 발생
시간복잡도가 O(n)가 되어야한다.
#include <algorithm>
template <class T>
static bool compareVectors(std::vector<T> a, std::vector<T> b)
{
if (a.size() != b.size())
{
return false;
}
::std::sort(a.begin(), a.end());
::std::sort(b.begin(), b.end());
return (a == b);
}
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs)
{
vector<vector<string>> ans;
vector < vector <int> > lettermap(strs.size(),vector <int>(26,0));
vector<int> flag(strs.size() , 0);
int ans_c = 0; //동일 단어의 그룹 index
for(int i = 0; i<strs.size(); i++) //lettermap 을 작성
for(int k = 0; k<strs[i].size(); k++)
lettermap[i][strs[i][k] - 'a'] +=1;
for(int i = 0; i<strs.size(); i++) //탐색단어과 비교단어가 같은 지
{
if(flag[i] == 0)
{
ans.push_back(vector<string> ()); //ans 벡터의 행을 하나 추가한다.
if(i == strs.size() - 1)
//마지막 단어의 경우 중복되었는지 아닌지만 판단하면 된다.
//중복이 아닌 경우 단일 그룹에 추가
ans[ans_c].push_back(strs[i]);
else
{
ans[ans_c].push_back(strs[i]); //탐색 단어를 그룹의 처음에 넣고 시작
for(int k = i+1; k<strs.size(); k++)
if(lettermap[i] == lettermap[k]) //두 lettermap 벡터가 같다면,
{
ans[ans_c].push_back(strs[k]); //동일 단어그룹에 추가한다.
flag[k] = 1; //중복 방지 flag
}
}
ans_c +=1; //그룹 변경
}
}
return ans;
}
};
##정답 코드 및 출처
정답 코드는 lettermap간의 비교가 아닌, 단어를 오름차순 정렬한 상태를 map에 집어 넣었다.
아직 hashmap과 map에 대해 잘 모르는 상태라 더 공부해보아야겠다.
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string s : strs) {
string t = s;
sort(t.begin(), t.end());
mp[t].push_back(s);
}
vector<vector<string>> anagrams;
for (auto p : mp) {
anagrams.push_back(p.second);
}
return anagrams;
}
};
Mistake
2차원 벡터를 선언하고, ans.push_back(vector<string> ()); 로 하나의 행을 만들어줘야 값을 집어 넣을 수 있다.
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals)
{
sort(begin(intervals), end(intervals));
int count = 0;
int end = intervals[0][1];
for(int i = 1; i<intervals.size(); i++)
{
if(intervals[i-1][0] == intervals[i][0]) //start가 같은 경우
{count ++; }
else //start가 다른 경우
{
if(end > intervals[i][0]) //남은 하나와 겹치는 경우
{count++; }
else end = intervals[i][1]; //end 재설정
}
}
return count;
}
};
###정답 코드 / 출처
내 코드와의 차이점 : start가 같은 경우와 겹치는 경우를 하나로 묶어 조건을 간결하게 추렸다.
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end());
int i = 1; //interval 의 index
int c = 0; //count
while(i<intervals.size()){
if(intervals[i][0]<intervals[i-1][1]) //i-1과 i가 곂치면
{
intervals[i][1] = min(intervals[i-1][1], intervals[i][1]); //end 재설정
c++; //제거 카운트 증가
}
i++;
}
return c;
}
};
Explanation: [4,8] 이 [3,5],[6,7],[8,10]와 겹쳐 [3,10] 으로 합쳐졌다..
Organizing thoughts
규칙을 최대한 찾아보았다.
1. interval과 new의 요소 개수는 짝수 개
2. output은 행의 개수를 최소화 시키는 게 아니라, 영향을 준 부분만 수정한다.
3. 더 깊이 생각해보면 영향을 안준 부분은 output에 그대로 내보내면 된다.
4. 오름차순이라는 점을 이용한다.
5. 영향을 준 부분을 output에 보내는 방법을 생각해보자
a. new의 범위는 최대 [0, interval의 최대 보다 큰 값] 최소 [n, n+1]
b. new의 요소가 interval 내에 들어가면 범위를 변경해야 한다.
6. new가 들어갈 곳이 아무 곳도 없는 경우
a. new의 end 부분이 interval의 처음 값 보다 작다면, 최우선에 insert한다.
b. new의 start 부분이 interval의 마지막 값 보다 크다면, 마지막 부분에 insert한다.
[전체적인 flow]
<second < new_start> 일 때는 정답에 추가하다가
<new_end 가 들어갈 위치를 찾으면> new_start ~ new_end 의 범위에 맞게 정답에 추가 후
<이후> 나머지를 그대로 넣으면 된다.
Sourcecode
#직접 작성한 코드
interval의 size가 1인 경우 및 다른 특정한 경우 올바르게 작동하지 않는다.
다행히 정답 코드와 전체적인 flow는 유사했다.
class Solution {
public:
vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
vector<vector<int>> ans(intervals.size()+1,2);
int start = newInterval[0];
int end = newInterval[1];
int flag = 0; //new의 범위를 설정하고, 탐색하는 부분이 되면 flag를 올린다.
if(end > interval[0][0])
{
interval.push_front(newInterval);
return interval;
}
if(start > interval[interval.end()][1])
{
interval.push_back(newInterval);
return interval;
}
int k = 0;
for(int i=0; i<interval.size(); i++)
{
if(interval[i][0] <= start && start <= interval[i][1])
{
start = interval[i][0];
flag = 1;
}
else if(start < interval[i][0])
flag = 1;
if(flag == 0)
ans[k++] = interval[i];
if(interval[i][0] <= end && end <= interval[i][1])
{
end = interval[i][1];
ans[k][0] = start;
ans[k][1] = end;
flag = 0;
k++;
}
else if(end < interval[i][0])
{
ans[k][0] = start;
ans[k][1] = end;
flag = 0;
k++
ans[k] = interval[i];
}
}
return ans;
}
};
# 정답 코드 및 코드 출처
class Solution {
public:
vector<vector<int>> insert(vector<vector<int>>& ints, vector<int>& ni, int i = 0) {
vector<vector<int>> res;
for (; i < ints.size() && ints[i][0] <= ni[1]; ++i)
{
//i를 끝까지 탐색하되, new의 end가 가능한 범위 내에서 진행한다.
//만약 interval[i][0]가 6인데 end 부분이 4면 범위를 벗어난 것이다.
//즉 new가 해당하는 범위를 탐색함과 동시에 new의 이전 범위를 추가하는 작업이라 생각하면 된다.
if (ints[i][1] < ni[0]) //new의 범위 이전에 interval이 있으면
res.push_back(ints[i]); //정답 배열에 interval 추가
else
//탐색 중인 interval이 new 범위 안에 들어왔다면,
//첫 else에서 start가 결정되고, 마지막 else에서 end가 결정
{
ni[0] = min(ni[0], ints[i][0]);
ni[1] = max(ni[1], ints[i][1]);
}
//else 의 의미를 더 생각해보자면,
//interval[i][0] <= new_interval[1] : new의 end가 가능한 범위
//interval[i][1] >= new_interval[0] : interval 값이 new의 start를 넘어야 new의 범위 안에 들어온 것
}
res.push_back(ni); //정해진 start와 end 범위를 삽입
res.insert(end(res), begin(ints) + i, end(ints)); //end 이후 남아 있는 interval 요소들을 삽입한다.
return res;
}
};
최대 nums의 값만큼 다음 index로 넘어가 끝까지 도달하면 true 도달하지 못하면 false 를 리턴한다.
Ex : Input: nums = [2,3,1,1,4] Output: true
Explanation: nums[0]에서 1칸 or 2칸 점프가능하고, 2칸 점프했을 때, nums[2] 에서 1칸 점프, nums[3]에서 1칸 점프 nums[4] 도달하므로 true
Input: nums = [3,2,1,0,4] Output: false
Explanation: nums[3] 에서 0이고 더 이상 진행이 불가능하다.
Organizing thoughts
size가 1일 때에는 무조건 true 가 되도록 하고, 그 이후를 어떻게 하면 좋을지 생각해보자
예시에서 보았듯 nums[i] 가 무슨 값인지에 따라, nums가 진행가능한 범위가 정해진다.
즉 진행가능한 범위에 nums의 마지막 index가 들어오면 true를 리턴하고, 불가능하면 false 리턴하면 된다.
Sourcecode
dp 를 사용해 직접 작성한 코드이다.
하지만 시간복잡도가 O(n²) 이므로 Time Limit Exceeded가 떴다.
class Solution {
public:
bool canJump(vector<int>& nums)
{
vector<int> dp(nums.size(),0);
dp[0] = 1; //초기값 true
for(int i=0; i<nums.size()-1 ; i++) //마지막 이전까지 진행
{
if(dp[i] > 0) //index가 범위 안에 있다면
{
for(int k=0; k<nums[i]; k++) //해당 index가 더 나아갈 수 있는 범위를
if(i+1+k < nums.size()) //size를 벗어나지 않는 선에서 추가한다.
dp[i+1+k] = 1;
}
}
return dp[nums.size() - 1]; //마지막이 범위에 들어오는지 아닌지에 따라 true / false
}
};
아래는 가장 간단하고 직관적인 코드이다.
출처
bool canJump(vector<int>& nums) {
int size=nums.size();
int step=nums[0];
//nums의 값만큼 앞으로 갈 수있게 step 설정
for(int i=1;i<size;++i){
step--; //다음 index로 넘어갈 때마다 step 감소
if(step<0) //step 이 0보다 작아지면, 더 나아갈 수 없으므로 false
return false;
if(nums[i]>step) // nums[i] 값이 step보다 크다면 더 많이 나아갈 수 있으므로 step 교체
step=nums[i];
}
return true; //defalut 값 true
}
dp[1]은 dp[0] + dp[-1]개가 된다. (일단은 dp[-1] = 1이 있다고 치자)
111은 1/1 에 '1'이 추가된 것 또는 1/ 에 '11'이 추가된 것이므로
1개 단위는 1이고 dp[1]과 같다.
2개 단위는 '1' 에 '11'이 추가된 것이므로
dp[2]은 dp[1] + dp[0]개가 된다.
111은 1/1 에 '1'이 추가된 것 또는 1/ 에 '11'이 추가된 것이므로
1개 단위는 1이고 dp[1]과 같다.
2개 단위는 '1' 에 '11'이 추가된 것이므로
dp[2]은 dp[1] + dp[0]개가 된다.
여기서 중요하다.
1110은 1/1/1 에 '0'이 추가된 것 또는 1/1 에 '10'이 추가된 것이므로
1개 단위는 '0'이 추가됐기에 불가능하고,
2개 단위는 '1/1' 에 '11'이 추가 된 것이므로
dp[3]은 dp[1] 개가 된다.
11106은 1/1/1/0 에 '6'이 추가된 것 또는 1/1/1 에 '06'이 추가된 것이므로
1개 단위는 이전과 같고, 2개 단위는 '1/1/1' 에 '06'이 추가된 것은 불가능하므로
dp[4]은 dp[3] 개가 된다.
최종 dp는 [1,2,3,2,2] 이 된다.
그렇다면 초기값은 어떻게 설정할까?
아까 dp[-1] = 1이 있었다고 가정했다.
하지만 index가 -1이 되는 것은 불가능하기 때문에
dp의 길이를 n+1로 선언하고, dp[n]에 1을 대입 후 위 방식을 거꾸로 진행하면 된다.
따라서 초기값은 dp[n] = 1만 설정하고,
dp[n-1] 을 탐색할 때에는 두 자리 단위 검사를 패스하게 조건을 맞추어준다.
Sourcecode
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
vector<int> dp(n+1,0);
//size+1로 dp를 선언하는 이유는 처음 1자리 수 검사를 할 때,
//끝 자리가 0인지 아닌지에 따라 초기값이 바뀌기에
//dp[n]을 1로 설정해 dp[n-1]에서 1을 끌고 가는지 아닌지 정한다.
dp[n] = 1;
for(int i = n-1; i>=0; i--)
{
if(s[i] != '0') //한 자리 단위 검사
dp[i] += dp[i+1];
if(i<n-1 && (s[i] == '1' || s[i] == '2' && s[i+1] <= '6'))
dp[i] += dp[i+2]; //두 자리 단위 검사
}
return dp[0];
}
};
나머지 칸들은 칸 기준 왼쪽 칸으로 가는 경우의 수와 위 칸으로 가는 경우의 수의 합이다.
예를 들어 2 x 3 이라면,
1
1
1
1
1+1 = 2
2+1 = 3
이렇게 된다.
따라서
1. 2 x 3 행렬을 하나 만들고
2. 2 x 3 행렬을 모두 1로 초기화
3. dp[1][1] 부터 dp[n-1][n-1]까지 대각선(왼쪽 , 위) 합을 구한다.
4. dp[n-1][n-1] 을 리턴한다.
Sourcecode
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n,1)); // m x n 인 2차원 벡터 행렬 선언
for(int i = 1; i<m; i++)
for(int k = 1; k<n; k++)
dp[i][k] = dp[i][k-1] + dp[i-1][k]; //대각합을 구하는 과정
return dp[m-1][n-1];
}
};
주어진 문자열 s에서 최대 k번 문자열을 교체하여 가능한 가장 긴 중복 문자열의 길이를 리턴한다.
Input: s = "AABABBA", k = 1 Output: 4
Explanation: 4번째 A를 B로 1번 교체하므로 길이가 4인 BBBB 가장 긴 중복 수열을 만들었다.
Organizing thoughts
핵심 아이디어는 아래 유튜브를 참고했다.
Sourcecode
소스코드 출처
class Solution {
public:
int characterReplacement(string s, int k) {
int sl = s.length(); // 문자열의 길이
vector<int>char_freq(26,0); //A~Z의 빈도를 저장할 LetterMap
int result = INT_MIN; //결과 값
//투 포인터
int first = 0;
int last = 0;
// 저장한 빈도 중 가장 큰 빈도를 저장할 변수
int max_freq = 0;
// last 포인터가 끝까지 갈 때까지 반복
while(last < sl)
{
char_freq[s[last]-'A']++; // 탐색 글자의 빈도 증가
max_freq = max(max_freq,char_freq[s[last]-'A']);
//문자열 중 가장 큰 빈도를 가진 문자의 개수
// window 크기 - max_freq 가 k 보다 작으면 window의 크기를 넓히면 되지만
// k보다 클 경우 window의 크기를 줄여야한다.
if((last-first+1 - max_freq)> k)
{
char_freq[s[first]-'A']--; //windown를 오른쪽으로 옮기면서
first++; //왼쪽 빈도 줄이기
}
result = max(result,last-first+1); //window길이와 이전 max의 값을 비교
last++; //window 크기 증가
}
return result;
}
};
int lengthOfLongestSubstring(char * s)
{
int letterMap[128] = {0};
int max = 0;
char* start = s;
char* end = s;
while(*end)
{
if(letterMap[*end] != 0) //탐색 값이 중복일 때
{
max = (end-start) > max ? (end - start) : max;
// 중복을 찾은 지점과 처음 지점의 차이 = Substring 길이
// Substring가 max보다 크면 max에 단어길이 저장
while(*start != *end)
{
letterMap[*start] = 0; //Substring에 있던 글자들 0으로 초기화
start++; //Substring 의 탐색지점 이동 ex(abc 였으면 a->b->c)
}
start++; //시작점을 중복이었던 부분으로 이동
}
else //탐색 값이 중복 아닐 때
{
letterMap[*end] = 1; //해당 문자가 있었다고 표시
}
end++; //탐색 값 다음 위치로
}
return (end-start) > max ? (end - start) : max;
// 중복이었던 경우가 아예 없었더라면, end-start가 max값이 됨
}
New things learned
포인터의 위치차로 단어의 길이를 구하는 방법이 신박했다.
시간복잡도를 줄이는 방법 중 하나는 사용하는 메모리를 늘리는 것이다.
O(n₃)인 풀이에서 중복인 부분을 판단하는데 반복문을 사용하는데, 이 풀이에서 letterMap을 사용해 중복이었는지 아닌지 판단했다.
이를 양파 까듯 n x n 테두리, (n-2) x (n-2) ... 1 x 1 까지 반복한다.
Sourcecode
void rotate(int** matrix, int matrixSize, int* matrixColSize)
{
int n = matrixSize;
int* store = malloc(sizeof(int) * (n-1)); //위 끝을 저장할 배열
for(int i=0; i<(n+1)/2; i++)
{
for(int k = i+1; k<= n-i-1; k++)
store[k-i-1] = matrix[i][k]; // 위 끝값 저장
for(int k = 0 ; k< n-2*i; k++)
matrix[i][n-1-k-i] = matrix[k+i][i]; // 위 끝을 왼 끝으로 채우기
for(int k = 0 ; k< n-2*i; k++)
matrix[k+i][i] = matrix[n-1-i][k+i]; //왼 끝을 아래 끝으로 채우기
for(int k = 0 ; k< n-2*i; k++)
matrix[n-1-i][k+i] = matrix[n-1-k-i][n-1-i]; //아래 끝을 우끝으로 채우기
for(int k = 0 ; k< n-2*i -1; k++)
matrix[k+1+i][n-1-i] = store[k]; //우끝을 저장한 위 끝으로 채우기
}
}
Mistake
진행 횟수와 좌표의 위치는 다르기에 자세히 생각하기 n x n -> (n-2) x (n-2) 과정에서 시작 점의 좌표를 0,0 그대로 했음
int* spiralOrder(int** matrix, int matrixSize, int* matrixColSize, int* returnSize)
{
int* ans = malloc(sizeof(int)* *matrixColSize * matrixSize); //정답 array
int i = 0; //정답 array index
int up = 0; //윗 방향
int down = 0; //아랫 방향
int left = 0; //왼쪽 방향
int right = 0; //오른쪽 방향
int x = 0; //matrix x좌표
int y = 0; //matrix y좌표
int left_end = 0; //왼쪽 끝 index
int right_end = * matrixColSize - 1; //오른쪽 끝 index
int up_end = 0; //위쪽 끝 index
int bottom_end = matrixSize - 1; //아래쪽 끝 index
right = 1;
if(*matrixColSize == 1 || matrixSize == 1)
{
if(*matrixColSize == 1){right = 0; down = 1;}
up_end--;
bottom_end ++;
left_end--;
right_end++;
}
while(i < *matrixColSize * matrixSize)
{
ans[i] = matrix[y][x];
if(left == 1) x--;
else if(right == 1) x++;
else if(up == 1) y--;
else if(down == 1) y++;
if(x==left_end && y>up_end) //왼쪽 아래 끝 일때
{
left = 0;
up=1;
left_end++;
}
else if(x==right_end && y<bottom_end) //오른쪽 위 끝 일때
{
right = 0;
down = 1;
right_end--;
}
else if(y==up_end && x<right_end) //왼쪽 위
{
up = 0;
right = 1;
up_end++;
}
else if(y==bottom_end && x>left_end ) //오른쪽 아래 끝
{
down = 0;
left = 1;
bottom_end--;
}
if(bottom_end < up_end) bottom_end = -1;
i++;
}
*returnSize = i;
return ans;
}
Mistake
행이나 열이 1줄인 경우를 생각 못했다.
66번줄의 if(bottom_end < up_end) bottom_end = -1는 bottom_end 값이 up_end 보다 커지는 순간이 발생하는데, 이는 행이 홀수개인 경우 생기며, 탐색하는 값이 오른쪽 아래 끝에 위치해 있다고 판단하게 된다. 이 부분을 막기 위해, bottom_end를 -1로 바꿔 오른쪽 아래 끝 조건문에 걸리지 않게 했다.