문제

빈도 정렬 백준 2910

"맞았습니다" 코드

#include <bits/stdc++.h>
#define X first
#define Y second
using namespace std;

int n, c, num;
map<int,int> idxm; // {숫자, 처음 나온 인덱스} 저장
map<int,int> recentm; // {숫자, 빈도} 저장
vector<pair<int,int>> v;

bool cmp(pair<int,int> a, pair<int,int> b){ // 빈도 내림차순, 인덱스 오름차순
  if(a.X == b.X) return idxm[a.Y] < idxm[b.Y];
  return a.X > b.X;
}
int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> n >> c;
  for(int i = 0; i < n; i++){
    cin >> num;
    recentm[num]++; // num의 빈도를 1 증가시킨다
    if(idxm[num] == 0 ){ // num이 처음 나왔다면, 인덱스를 저장한다.
      idxm[num] = i+1;
    }
  }

  // recentm {숫자, 빈도} -> {빈도, 숫자} 로 바꿔서 벡터에 저장
  for(auto & it : recentm){
    v.emplace_back(it.Y, it.X);
  }

  // 빈도를 기준으로 정렬하되, 인덱스 조건 고려하여 정렬하도록 cmp 함수 적용
  sort(v.begin(), v.end(), cmp);

  for(int i = 0; i < v.size(); i++){ // 숫자의 빈도(== v[i].X)만큼 숫자를 출력.
    for(int j = 0; j < v[i].X; j++){
      cout << v[i].Y << ' ';
    }
  }
  return 0;
}

리뷰

처음에는 벡터만을 생각해서 풀어서 감이 안오고 어렵게 느껴졌다.
key의 중복을 허용하지 않는 map을 이용해서 풀었다.

 

 

처음에 입력 받을 때 2개의 맵을 초기화한다.
(숫자, 빈도)를 저장하는 recentm map에 빈도를 저장한다.
(숫자, 숫자가 처음 나온 인덱스)를 저장하는 idxm map에 '숫자가 처음 등장한 인덱스'를 저장한다.

인덱스가 0이면 '처음 나왔다' 고 보기 때문에. 1+i를 인덱스로 저장했다.

 

빈도가 가장 많은 숫자가 앞에 와야 하니까 정렬이 필요하다.
map recentm {숫자, 빈도} 을 vector v {빈도, 숫자}로 바꿔서 저장한다.

여기서 vector.emplace_back()을 썼는데. vector.push_back()과 동일한 기능이다.

push_back() 과 달리 emplace_back()는 데이터를 삽입할 때 임시 객체를 생성하지 않아서 약간 더 빠르다고 한다.

 

빈도가 같은 경우에, '숫자가 처음 등장한 인덱스'가 작은것을 우선하라는 조건을 주기 위해 cmp 비교 함수를 넣고 sort() 한다.

정렬이 끝나면, 숫자를 '숫자의 빈도'만큼 출력해주면 되니까 이중 반복문으로 vector 를 순회하여 출력한다.

iterator로 순회하는 반복문 코드 2가지 형태를 배웠다.

  map<int,int> recentm; // {숫자, 빈도} 저장

  // 아래 2개 반복문은 똑같이 동작한다. 
  for(auto & it : recentm)

  for(auto it = recentm.begin(); it != recentm.end(); it++) 
}
728x90

목차 

0x00 다이나믹 프로그래밍 설명 

0x01 연습 문제

0x02 경로 추적 


0x00 다이나믹 프로그래밍 설명 

다이나믹 프로그래밍은 여러 개의 하위 문제를 먼저 푼 후, 그 결과를 활용하여 문제를 해결하는 알고리즘이다. 

앞 글자만 따서 DP라고 부르기도 한다. 

 

재귀를 배울 때 피보나치 문제를 풀어봤다.
재귀적으로 구현하면, fibo(4)를 호출할 때 fibo(3)과 fibo(2)를 호출한다. 

fibo(3)은 또 fibo(2)fibo(1)을 호출하고. fibo(2)fibo(1)과 fibo(0)을 호출한다. 

-> 중복된 연산 발생

int fibo(int n){
  if(n <= 1) return 1;
  return fibo(n-1) + fibo(n-2);
}

피보나치 문제를 DP로 해결하면, 연산의 결과를 배열에 저장하는 메모이제이션을 활용한다. 

그래서 fibo(1)을 재호출하지 않고 f[1]의 값을 재활용한다. 

int fibo(int n){
  int f[20];
  f[0] = f[1] = 1;
  for(int i = 2; i <= n; i++)
    f[i] = f[i-1] + f[i-2];
  return f[n];
}

연산의 중간 결과를 저장하여 재활용 하니까. O(N)에 답을 구할 수 있다. 

 

DP를 푸는 과정 

1. 테이블 정의하기 

2. 점화식 찾기

3. 초기값 정하기 

 

DP는 점화식 찾고, 초기값을 채워넣은 후에 반복문을 돌면서 배열을 채우면 구현이 끝나는 단순한 구조다. 

DP에 익숙하지 않으면 DP문제인지도 알아채지 못할 수도 있다. 여러 문제를 풀면서 DP 점화식을 찾는 연습을 하자.


 0x01 연습 문제 : 1로 만들기 BOJ 1463

이 문제는 BFS로도 해결할 수 있다. dist 배열을 두고 초기값을 1로 한 뒤, x2, x3, +1로 뻗어나가면 답이 나온다. 

그런데 DP로 풀면 훨씬 짤막하게 구현할 수 있다.

1. 테이블 정의하기

먼저 테이블은 위와 같이 정의하자. D[i] = i를 1로 만들기 위해 필요한 연산 횟수의 최솟값

배열의 인덱스와 값을 어떻게 구성할지에 관한 건 여러 경험이 쌓이면 감이 온다. 

 

2. 점화식 찾기

구체적인 예시를 들어서 D[12]를 구하는 연산을 생각해보자. 

D[12] = 12를 1로 만들기 위해 필요한 연산 횟수의 최솟값

3으로 나눈다: D[12]  =  D[4] + 1번의 연산(3으로 나누기)

2로 나눈다: D[12] = D[6] + 1번의 연산(2로 나누기)

1을 뺀다 D[12] = D[11] + 1번의 연산(1 빼기)

세 가지 연산 횟수 중에서 최솟값이 답이 된다. 따라서 D[12] = min( D[4] + 1, D[6] + 1, D[11] + 1)  

 

3. 초기값 정의하기 

1을 1로 만드는데 연산은 0번 필요하다. D[1] = 0

2를 1로 만드는데 연산은 1번 필요하다.(2로 나누거나 1을 빼기) D[2] = 1

이 문제에서는 D[1] 초기값만 정해주면 된다.

이제 D[] 배열을 전역에 잡고 for문을 돌면서 주어진 정수x 인덱스까지 채우면 답이 나온다. 

 

1로 만들기 바킹독님 코드, 내 코드

   // http://boj.kr/161694ef04f04d8dbe826e253622c1cb
#include <bits/stdc++.h>
using namespace std;

int d[1000005];
int n;

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n;
  d[1] = 0;
  for(int i = 2; i <= n; i++){
    d[i] = d[i-1]+1;
    if(i%2 == 0) d[i] = min(d[i],d[i/2]+1);
    if(i%3 == 0) d[i] = min(d[i],d[i/3]+1);
  }
  cout << d[n];
}

DP는 구현이 이렇게 짤막한 편이다. 점화식을 찾아내는 것이 관건이다. 


0x01 연습 문제 : 1,2,3 더하기 BOJ 9095

1. 테이블 정의하기

정수 4를 1,2,3의 합으로 나타내는 방법의 수는 본문에 나와 있지만 7가지다.

테이블은 D[4] = 7 의 형태로 만들면 된다. D[i] = i를 1,2,3의 합으로 나타내는 방법의 수

7가지 방법을 3줄에 나눠서 써놨다. 규칙이 뭘까? 점화식을 생각해보자. 

첫 번째 줄은 +1로 끝난다.

두 번째 줄은 +2로 끝난다.

세 번째 줄은 +3으로 끝난다. 

2. 점화식 찾기 : 노랑으로 표시된 +1, +2, +3만 제외하고 생각해보자. 

1+1+1, 3, 2+1, 1+2 3을 1,2,3의 합으로 나타내는 방법 == D[3]
1+1, 2 2를 1,2,3의 합으로 나타내는 방법 == D[2]
1 1을 1,2,3의 합으로 나타내는 방법 == D[1]

3을 1,2,3의 합으로 나타내는 방법에 +1, +2, +3 을 붙이면  4가 된다.

1+1+1+1, 3+1, 2+1+1, 1+2+1 3을 1,2,3의 합으로 나타내는 방법 +1
1+1+2, 2+2 2를 1,2,3의 합으로 나타내는 방법 +2
1+3 1을 1,2,3의 합으로 나타내는 방법 +3

이렇게 D[4]=D[3] + D[2] + D[1] 이 성립하게 된다. 

 

3. 초기값 정하기 

D[i] 는 직전 값이 3개는 필요하다. D[i] = D[i-1] + D[i-2] + D[i-3] 

D[1] = 1, D[2] =2, D[3] = 4. 

 

1,2,3 더하기 바킹독님 코드, 내코드

// http://boj.kr/8b763a1df15a4cb3936f7a181f3db97c
#include <bits/stdc++.h>
using namespace std;

// d[i] = i를 1, 2, 3의 합으로 나타내는 방법의 수
int d[20];
int main(void){
  ios::sync_with_stdio(0);
  cin.tie(0);
  
  d[1] = 1; d[2] = 2; d[3] = 4;
  for(int i = 4; i < 11; i++)
    d[i] = d[i-1] + d[i-2] + d[i-3];

  int t;
  cin >> t;
  while(t--){
    int n;
    cin >> n;
    cout << d[n] << '\n';
  }
}

0x01 연습 문제 : 계단 오르기 BOJ 2579

만약 N이 20 정도로 그다지 크지 않으면 백트래킹 같은 방법으로 O(N^2)으로 해결이 가능하다. 

지금처럼 N이 300이라면 DP로 해결해야 한다. 

2개의 규칙을 유념하자. 

1) 최종 계단은 반드시 밟아야 한다.

2) 계단을 연속 3개 밟을 수 없다.

 

1. 테이블 정의하기

S[k] = 주어진 계단 점수

D[i] = i 번째 계단까지 최대 점수 

 

2. 점화식 찾기 

최종 계단은 반드시 밟아야 하니까, 그 이전 계단에 대해서만 생각하자.

계단이 4개 있을 때 가능한 경우 2가지를 그림으로 그렸다. 

경우 2 : 4번째 계단이 2연속 밟은 계단이면, 직전 계단은 반드시 밟는다.

경우 1 : 4번째 계단이 1연속 밟은 계단이면, 직전 계단은 반드시 안밟는다. 

 

경우 1 : 최종 계단이 2번째 연속으로 밟은 계단이면, 확실한건 3번은 밟았고 2번은 안밟는다는 것이다. 

따라서 1번째 계단까지의 최대점수(D[1]) + 3번 점수(S[3]) + 4번 점수(S[4]) 가 답이 된다.

계단 번호 1 2 3 4
밟았는지  ? X O1 O2

경우 2 : 최종 계단이 1번째 연속으로 밟은 계단이면, 확실한건 3번은 안밟았다는 것이다.

따라서 2번째 계단까지의 최대점수(D[2]) + 4번 점수(S[4]) 가 답이 된다.

계단 번호 1 2 3 4
밟았는지    ? X O1

점화식은 아래와 같다. 

D[i] = S[i] + max(S[i-1] + D[i-3] , D[i-2]);

 

3. 초기값 정하기 

i를 구할 때 i-3 이 필요하니까 1, 2, 3 까지 초기값을 정해줘야 한다.

D[1] = S[1], D[2] = S[2], D[3] = S[3];

 

계단 오르기 전체 코드 바킹독님 코드, 내 코드 

// http://boj.kr/e93e56bb850a46378cf8f53486233cdc
#include <bits/stdc++.h>
using namespace std;

int s[305];
int n;
int d[305];

int main(void){
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n;
  int tot = 0;
  for(int i = 1; i <= n; i++){
    cin >> s[i];
    tot += s[i];
  }
  if(n <= 2){
    cout << tot;
    return 0;
  }
  d[1] = s[1];
  d[2] = s[2];
  d[3] = s[3];
  for(int i = 4; i <= n-1; i++) d[i] = min(d[i-2],d[i-3])+s[i];
  cout << tot - min(d[n-1],d[n-2]);
}

0x01 연습 문제 : RGB거리 BOJ 1149

 

 

 

 

 

 

 


0x02 경로 추적 : 1로 만들기 2 BOJ 12852 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


DP 문제집 

백준 문제 번호  내 코드 
1463 1로 만들기 내 코드
9095 1,2,3더하기 내 코드
2579 계단 오르기 내 코드 
1149 RGB거리  내 코드
11726 2xn 타일링 내 코드
11659 구간 합 구하기 4 내 코드
12852 1로 만들기 2 내 코드
1003 피보나치 함수  내 코드

 


다음 시간에는 그리디 알고리즘을 공부한다. 

공부 내용 출처 : 바킹독 알고리즘 다이나믹 프로그래밍

728x90

'알고리즘 > 실전 알고리즘' 카테고리의 다른 글

0x0F강 - 정렬2  (0) 2021.12.18
0x0E강 - 정렬1  (0) 2021.12.11
0x0C강 - 백트래킹  (0) 2021.12.10
0x0B강 - 재귀  (0) 2021.12.08
0x0A강 - DFS  (0) 2021.12.07

문제

단어 정렬 백준 1181

"맞았습니다."코드

#include <bits/stdc++.h>
using namespace std;

int n;
string st;
vector<string> v;

bool cmp(string &a, string & b){
  if(a.size() != b.size()) return a.size() < b.size(); // 길이 짧은것 우선
  else return a < b; // 사전순
}

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> n;
  for(int i = 0; i < n; i++) {
    cin >> st;
    v.push_back(st);
  }
  sort(v.begin(), v.end(), cmp);
  v.erase(unique(v.begin(), v.end()), v.end());
  for(auto i : v) cout << i << '\n';
  return 0;
}

리뷰

중복을 제거하는 것은 unique와 erase를 썼는데 이번에 풀면서 배웠다.
unique 를 쓰면 중복은 제거되도 벡터의 크기가 줄어들지는 않는다. unique는 중복되는 원소의 포인터를 맨 뒤로 보내기 때문이다.
unique의 리턴 값은 중복 제거 된 원소의 포인터다.
그래서 erase로 중복 제거된 원소의 포인터 부터 맨 마지막 포인터까지 공간을 삭제해준다.
erase를 하면 아예 메모리가 해제되니까 중복된 부분의 크기만큼 반환되는 것이다.

c++ 벡터 중복 제거 unique와 erase 포스팅

728x90

문제

수 정렬하기5 백준 15688

"맞았습니다" 코드

#include <bits/stdc++.h>
using namespace std;

int n, num;
int arr[2000002];

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> n;
  for(int i = 1; i <= n; i++) {
    cin >> num;
    arr[num+1000000]++;
  }

  for(int i = 0; i <= 2000000; i++) {
    while(arr[i] > 0) {
      cout << i-1000000 << '\n';
      arr[i]--;
    }
  }

  return 0;
}

리뷰

절대값이 100만 이하인 정수가 입력으로 들어오니까.
양수 음수 처리를 따로 해야겠다고 생각했었다.
일단 정답 코드를 제출하고 다른 분의 코드를 봤다.
200만 크기의 배열을 쓰면서도 어차피 입력받은 대로 더하고 빼주고 하면 입력 받은 숫자를 출력할 수 있으니까.
입력받을 때 저장하는 인덱스 : num + 1000000
출력할 때 숫자 : num - 1000000
이렇게 하면 충분했다.

나의 예전 코드

#include <bits/stdc++.h>
using namespace std;

int n, num;
int arr[2000002];

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> n;
  for(int i = 1; i <= n; i++) {
    cin >> num;
    if(num < 0) arr[abs(num)+1000000]++;
    else arr[num]++;
  }

  for(int i = 2000000; i > 1000000; i--) {
    while(arr[i] > 0) {
      int temp = abs(i-1000000) * (-1);
      cout << temp << '\n';
      arr[i]--;
    }
  }
  for(int i = 0; i <= 1000000; i++) {
    while(arr[i] > 0) {
      cout << i << '\n';
      arr[i]--;
    }
  }

  return 0;
}
728x90

목차 

0x00 Counting Sort

0x01 Radix Sort 

0x02 STL Sort

0x03 정렬의 응용


0x00 Counting Sort

{ 1, 5, 4, 2, 3, 1, 4, 3} 

이 수열을 숫자 크기를 기준으로 오름차순으로 정렬하려고 한다. 어떻게 할지 생각해보자. 

머지 소트나 퀵 소트를 써도 된다. 

여기서 다시 짚고 넘어갈 점! 

퀵 소트는 NlogN이 보장 된다 안 된다? 

최악의 경우 O(N^2)이다. 꼭 기억하자. 

 

이 수열을 조금 변형한 다음에, 정렬하자. 아래의 표가 어떤 것을 의미할까? 

-> 수열에 숫자가 포함된 횟수를 저장했다. 

1이 2번 나왔다. 2가 1번 나왔다. 3이 2번 나왔다 ... 

이 표만 있으면 각 숫자가 차례로 나온 횟수만 적으면 정렬이 가능하다! 이런 알고리즘이 카운팅 소트다. 

 

이 때, '최솟값과 최댓값을 커버할 수 있는 크기의 배열을 선언해도 되는지' 문제의 메모리 제한을 확인해야 한다. 

512MB 제한이면, int 기준 1.2억인 배열을 잡을 수 있다. 

대략 1000만 이하일 때, 카운팅 소트를 쓸 수 있다고 생각하면 된다. 

 

백준 15688번 수 정렬하기5 문제를 카운팅 소트로 풀어보자. (내 코드, 바킹독님 코드)

// http://boj.kr/eeee207545644480a1971a2723769d93 바킹독님코드
#include <bits/stdc++.h>
using namespace std;

int freq[2000001];
int main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);
  int n;
  cin >> n;
  for(int i = 0; i < n; i++){
    int a;
    cin >> a;
    freq[a+1000000]++;
  }
  for(int i = 0; i <= 2000000; i++){
    while(freq[i]--){ // cnt[i]번 반복
      cout << i-1000000 << '\n';
    }
  }
}

입력 숫자가 음수 양수 모두를 포함하니까 애초에 모든 수에 100만을 더한 코드다. 

시간복잡도를 생각해보자. 

수의 범위가 K라고 할 때.

1) 처음에 N개의 숫자들을 배열에 넣는다. 

2) 정렬할 때 K개의 칸을 확인한다.

따라서 시간복잡도는 O(N+K)가 된다.

 

이렇게 수의 범위가 제한되어 있고 작은 경우에 카운팅 소트가 유용하다. 

0x01 Radix Sort(기수 정렬)

라딕스 소트는 자릿수를 이용해서 정렬하는 알고리즘이다. 

카운팅 소트를 응용한 것으로 이해하면 된다. 

아래 수열을 라딕스 소트로 정렬해보자

012 는 12를 의미하는데. 편의를 위해 앞에 0을 붙였다. 

1. 크기 10의 리스트를 선언한다. 

2. 숫자의 '1의 자리'에 맞춰서 리스트에 넣는다.

012는 1의 자리가 2다. 따라서 인덱스 2에 넣는다. 421은 인덱스 1에 넣자. 

3. 0번 리스트부터 보면서 수를 꺼내서 수열을 재배열한다.

이 과정을 거치면 수열이 '1의 자리'를 기준으로 재배열된다.

1의 자리 기준이고 주어진 수열의 자리가 유지되니까 421이 021보다 앞에 온다. 

4. 다음 단계는 '10의 자리'에 맞춰서 리스트에 넣는다.

100은 10의 자리가 0이니까 인덱스 0에 넣는다. 421은 인덱스 2에 넣는다. 

5. 0번 인덱스 부터 9번 인덱스를 보면서 수를 꺼내서 재배열한다. 

5. 다음 단계는 '100의 자리'에 맞춰서 리스트에 넣는다. 

100은 100의 자리가 1이니까 인덱스 1에 넣는다. 502는 인덱스 5에 넣는다. 

6. 하란대로 하니깐 일단. 1의 자리 -> 10의 자리 -> 100의 자리 순으로 재배열 하니까 정렬이 완료됬다. 

여기서 질문. 502는 왜 421보다 클까? 

왜 이럴까를 생각해보면 라딕스 소트를 이해하는데 도움이 된다. 

502와 421을 비교할 때, 먼저 100의 자리를 비교한다. 100의 자리만 보면, 5 > 4 이니까! 

즉, 수A 가 수B보다 크다는 의미는 더 큰 자릿수에서 A의 자릿수가 B의 자릿수보다 큰 경우가 먼저 생긴다는 것이다. 

이걸 인지한 상태로 라딕스 소트 과정을 보자. 

10의 자리 까지는 502가 421 앞에 위치했다. 

하지만 100의 자리 비교 단계에서는 421는 4번 인덱스에 들어가고. 502는 5번 인덱스에 들어가기 때문에.

결국 421이 502보다 앞에 오도록 올바르게 정렬된다. 

 

라딕스 소트의 시간복잡도 

1의 자리 기준으로 재배열 하면서 리스트 크기 만큼 순회했다. 

10의 자리 기준으로 재배열 할 때도 리스트 크기 만큼 순회했다. 

따라서 자릿수의 최대 개수가 D개라고 할 때, D번에 걸쳐서 카운팅 소트를 하는 것과 똑같다. 

리스트의 크기를 K개 라고 할 때, 엄밀하게 말하면 시간 복잡도는 O(D(N+K))이지만.

보통 리스트의 개수는 N에 비해 무시가 가능할 정도로 작다. 당장 지금 예시로 든 상황도 리스트 갯수 K가 10밖에 안된다.

결론적으로 라딕스 소트의 시간복잡도는 O(DN)이다. 

 

적어도 코딩 테스트에서는 라딕스 소트를 구현해야 하는 상황은 아예 없다. 개념만 이해하자. 

라딕스 소트 구현 코드

#include <bits/stdc++.h>
using namespace std;

int n = 15;
int arr[1000001] = {12, 421, 46, 674, 103, 502, 7, 100, 21, 545, 722, 227, 62, 91, 240};

int d = 3; // 자릿수의 최대 길이.
int p10[3]; // p10[i] = 10의 i승

// x의 10^a 자리의 값을 반환하는 함수
int digitNum(int x, int a) {
  return (x / p10[a]) % 10;
}

vector<int> l[10]; // 0부터 9까지 저장.

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  p10[0] = 1;
  for(int i = 1; i < d; i++) p10[i] = p10[i-1] * 10;

  for(int i = 0; i < d; i++){ // 1의 자리 -> 10의 자리 -> 100의 자리.
    for(int j = 0; j < 10; j++) l[j].clear();
    // 각 수를 list에 대입
    for(int j = 0; j < n; j++) // 0~9
      l[digitNum(arr[j], i)].push_back(arr[j]);
    // 0번 리스트 부터 차례로 다시 arr에 넣어 재배열
    int aidx = 0;
    for(int j = 0; j < 10; j++){
      for(auto x : l[j]) arr[aidx++] = x;
    }
  }
  for(int i = 0; i < n; i++) cout << arr[i] << ' ';

  return 0;
}

 

코드를 간단히 짚고 넘어가면. n은 배열 크기, arr은 정렬할 배열이다.

d는 자릿수의 최댓값이다. (100의자리가 최대니까 여기서는 d가 3이 된다.)

배열 p10은 10의 지수를 저장할 배열이다. p10[0] == 1 , p10[1] = 10, p10[2] = 100 

// x의 10^a 자리의 값을 반환하는 함수
int digitNum(int x, int a){
  return (x / p10[a]) % 10;
}

 

digitNum함수는 x에서 10의 a자리 숫자를 추출한다. ex) digitNum(253, 1) 의 리턴값은 5다. 

vector<int> l 은 0번부터 9까지를 저장하는 리스트다. 

 

p10[i]는 10의 i승을 저장할 변수다. p10배열 초기화 코드인데. 정수 거듭제곱을 계산해야 하면 이렇게 하는 것이 정석이다. 

p10[0] = 1; // 10의 0승이니까 1이다. 
for(int i = 1; i < d; i++) p10[i] = p10[i-1] * 10;

 

pow 함수는 실수형을 반환하기 때문이다. 아래 코드 처럼 쓰면 실수 오차로 꼬일 수 있다. 주의하자. pow함수 레퍼런스

p10[i] = pow(10, i);
// double pow (double base, double exponent);

정렬 과정에 '비교'를 하는지? 

Comparision Sort Non-comparision Sort
버블 소트
머지 소트
퀵 소트 
카운팅 소트
라딕스 소트

카운팅 소트와 라딕스 소트는 정렬 과정에 '비교'를 하지 않는다.   


0x02 STL sort

algorithm 헤더에서 제공하는 sort() 함수는 기본으로 오름차순으로 정렬해준다. 

최악의 경우에도 O(NlogN)이 보장된다. 단 stable sort가 필요하다면 sort()가 아닌 stable_sort()를 쓰자. 

sort() 레퍼런스 

int a[5] = {1, 4, 5, 2, 7};
sort(a, a+5);

vector<int> b = {1, 4, 5, 2, 7};
sort(b.begin(), b.end()); // or sort(b.begin(), b.begin() + 5 );

stable_sort() 레퍼런스

두 원소의 우선순위가 같다면 원소의 위치를 변경하지 않는다. 사용법은 sort()와 똑같다. 

[중요] 비교 함수

sort() 함수의 파라미터로 비교함수를 넘길 수 있다. stable_sort()도 마찬가지다. 

예를 들어, int형을 5로 나눈 나머지 순으로, 5로 나눈 나머지가 같다면 크기 순으로 정렬하고 싶으면 아래 코드처럼 하면 된다.  

비교함수 cmp는 a가 b의 앞에 와야할 때 true를, 그렇지 않을 때 false를 반환한다. 

bool cmp(int a, int b){ // a가 b의 앞에 와야할 때 true, 아니면 false 
  if(a % 5 != b % 5) return a % 5 < b % 5;
  else a < b;
}

int a[7] = {1, 2, 3, 4, 5, 6, 7};
sort(a, a+7, cmp);
// 출력 결과: 5 1 6 2 7 3 4

 

비교함수 구현 시 자주하는 실수 

1. a가 b의 앞에 와야할 때만 true를 반환한다. a==b 라면 false를 반환한다. 

예를 들어, 수열을 크기의 내림차순으로 정렬하고 싶을 때, a와 b가 같은 경우 false를 반환하니까 런타임 에러가 발생할 수 있다. 

bool cmp(int a, int b){ // [런타임 에러 발생]
  if(a >= b) return true;
  return false; 
}

a > b 일 때 true를 반환하도록 수정하자. 

bool cmp(int a, int b){ // 올바른 형태
  return a > b;
}

2. 비교 함수의 인자로 STL 혹은 클래스 객체를 전달시 reference를 사용하자. 

문자열을 받아서 끝자리의 알파벳 순으로 정렬하고 싶어서 비교함수를 작성했다.  

아래의 비교함수는 어떻게 개선하면 좋을까? 

bool cmp(string a, string b){
  return a.back() < b.back();
}
 // 함수의 인자로 STL이나 구조체 객체를 보내면 값의 '복사'가 일어난다!

함수의 인자로 STL이나 구조체 객체를 보내면 값의 '복사'가 일어남을 기억하자!

이 경우, 굳이 복사라는 불필요한 연산이 없어도 비교가 가능하다. 

따라서 복사를 하는 대신 아래처럼 reference를 보내는 것이 더 바람직하다. 

bool cmp(const string& a, const string& b){ // 레퍼런스를 보내서 비교하는 것이 효율적.
  return a.back() < b.back();
}

 

const string& a, const string& b라고 쓰면 a와 b는 변하지 않음을 명시적으로 나타내기 때문에 가독성이 도움이 된다. 

하지만 코딩테스트에서 const를 안써도 크게 상관은 없다. 

비교함수 연습을 위해 백준 1431번 시리얼 번호 문제를 풀고 오자. 1431 내 코드 

 


0x03 정렬의 응용

정렬을 이용해서 시간복잡도를 개선할 수 있는 문제 백준 11652번 카드 문제를 확인해보자. 

O(N^2)의 풀이는 너무 직관적으로 보인다. 그런데 O(N^2)이면 시간초과이고. 수의 범위가 매우 크다. 

map은 아직 배우지 않았고. 또 이진검색트리라는 개념을 사용하는 만큼 map을 이용한 풀이는 배제하겠다. 

어떻게 해결해야 할까? 

 

일단 변수 3개가 필요하다. 

cnt : 현재 보고 있는 수의 등장 횟수. 

mxval : 현재까지 가장 많이 등장한 수의 값.

mxcnt : 현재까지 가장 많이 등장한 수의 등장 횟수. 

초기값으로 cnt = 0, mxval = -2^62-1, mxcnt = 0 을 주자. 

 

이제 2부터 읽으면서 진행하자. 

cnt 는 현재 보고 있는 수의 등장 횟수다. 

현재 보고 있는 수가 제일 앞에 있는 수이거나 현재 보고 있는 수와 바로 직전에 나온 수가 같을 때에는 cnt 를 1 증가시키면 된다. 

그래서 지금 '2'를 바라보고 있으니까 cnt를 1 증가 시키면 끝이다. 

다음 숫자도 '2'니까 직전수와 똑같다. cnt를 1 증가시킨다. 

다음 숫자도 '2'니까 cnt를 1 증가시킨다. 

이제 '3'을 바라본다. 더이상 2가 연속하지 않는다. 

이 때 mxval과 mxcnt를 업데이트해야 한다. 

여태까지 가장 많이 등장한 횟수는 '2'가 '3'번 나온 것이다. mxval은 2로, mxcnt는 3으로 업데이트 하자. 

'3'에 대한 개수를 세야 하니까 cnt=1로 변경해준다. 이제 3이 몇 개 있나 세면 된다. 

 

바킹독님 코드 11652 카드

// http://boj.kr/f3feaf22016f4c9687b84ab6be2f4389
#include <bits/stdc++.h>
using namespace std;

int n;
long long a[100005];

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n;
  for(int i = 0; i < n; i++)
    cin >> a[i];
  sort(a, a+n);
  int cnt = 0;
  long long mxval = -(1ll << 62) - 1; // 1을 long long으로 형변환하지 않고 1 << 62로 작성시 int overflow 발생
  int mxcnt = 0;
  for(int i = 0; i < n; i++){
    if(i == 0 || a[i-1] == a[i]) cnt++; // i가 0인 경우 앞쪽 식이 true이기 때문에 a[i-1]을 참조하지 않음
    else{
      if(cnt > mxcnt){
        mxcnt = cnt;
        mxval = a[i-1];
      }
      cnt = 1;
    }
  }
  if(cnt > mxcnt) mxval = a[n-1]; // 제일 마지막 수가 몇 번 등장했는지를 별도로 확인
  cout << mxval;
}

코드 마지막 줄에 '제일 마지막 수가 몇 번 등장했는지 별도로 확인'해야 한다. 

이 처리가 빠지면 제일 마지막 수의 등장 횟수를 빠뜨리게 된다. 

이 문제에서 사용한 정렬을 하면, 같은 수는 인접하게 된다는 성질을 이용해 수열에서 중복된 원소를 제거할 수 도 있다. 


정렬2 문제집

문제를 풀 때는 퀵, 머지, 카운팅, 라딕스 소트를 직접 구현할 일은 없고 STL sort만 쓰겠지만, 이렇게 정리하고 나면 기술면접 대비에 도움이 된다. 

특히 7795번 문제를 풀고, 다른 사람의 풀이를 찾아서 비교해보자. 

백준 문제 번호 내 코드 
1431 시리얼 번호  1431 내 코드  , 1431 내 코드2
11652 카드  11652 내 코드 (다시 풀기)
5648 역원소 정렬 5648 내 코드 
1181 단어 정렬 1181 내 코드
2910 빈도 정렬 2910 내코드
10814 나이순 정렬 10814 내 코드
11656 접미사 배열  11656 내 코드
10825 국영수 10825 내 코드
7795 먹을 것인가 먹힐 것인가 7795 내 코드

 


다음 시간에는 다이나믹 프로그래밍을 배운다.

공부 내용 출처 : 바킹독 알고리즘 정렬2

728x90

'알고리즘 > 실전 알고리즘' 카테고리의 다른 글

0x10강 - 다이나믹 프로그래밍  (0) 2021.12.19
0x0E강 - 정렬1  (0) 2021.12.11
0x0C강 - 백트래킹  (0) 2021.12.10
0x0B강 - 재귀  (0) 2021.12.08
0x0A강 - DFS  (0) 2021.12.07

문제

수 정렬하기 3 백준 10989

"맞았습니다"코드

#include <bits/stdc++.h>
using namespace std;
int n, num;
int arr[10001];

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> n;
  for(int i = 0; i < n; i++){
    cin >> num; arr[num]++;
  }
  for(int i = 1; i <= 10000; i++){
    while(arr[i]){
      cout << i << '\n'; arr[i]--;
    }
  }
  return 0;
}

리뷰

이 문제는 sort() 함수를 쓰면 틀린다.
왜냐하면 메모리 제한이 8MB 이다.
int 는 4bytes니까. 문제 조건인 천 만개 배열을 선언하면 40MB 를 차지한다.
10000 보다 작은 자연수만 입력으로 들어오니까,
10000을 배열의 인덱스로 두고.
인덱스에 해당하는 숫자의 '개수'를 배열의 값으로 저장해서 해결하면 된다.

728x90

5. 일반 헤더 

단순한 정보성 헤더에 대해 알아보자.

 

From 

유저 에이전트의 이메일 정보. 잘 안씀. 검색 엔진 같은 곳에서 크롤링 할 때 주로 쓴다. 

Referer

현재 요청된 페이지의 이전 웹페이지 주소. 엄청 자주 씀. 

Referer를 활용해서 유입 경로를 분석할 수 있다. 

A->B 로 URL이 이동하는 경우, B를 요청할 때 Referer:A를 포함해서 요청한다. 

참고) referer는 referrer의 오타인데. 이미 스펙에 오타 째로 들어가서 범용으로 사용중이다. 

User-Agent 

유저에이전트 애플리케이션 정보 (== 클라이언트 애플리케이션 정보)

서버 입장에서 User-Agent 정보를 활용하면 어떤 종류의 브라우저에서 장애가 발생하는지 파악할 수 있다. 

로그를 비교해서 브라우저 별 통계 정보를 만들 수 있다. 

요청에서 사용하는 헤더다. 

Server 

요청을 처리하는 Origin 서버의 소프트웨어 정보 

HTTP 요청 응답 과정에서 여러 프록시 서버를 거치게 되는데, 실제 요청을 처리해주는 최종 목적지 서버를 origin서버라고 한다. 

응답에서 사용하는 헤더다. 

Date 

메시지가 발생한 날짜와 시간. 응답에서만 사용한다. 

 


6. 특별한 정보 헤더 

Host 

요청한 호스트 정보(도메인) 

요청에서 사용하는 필수 헤더다. 

하나의 IP 주소에 여러 도메인이 적용되어 있을 때 Host 헤더로 구분하여 서버가 처리할 수 있다. 

가상 호스트를 통해 여러 도메인을 한번에 처리할 수 있는 서버가 있다. 여러 애플리케이션이 같은 도메인을 사용할 수 있다. 

 

Host 헤더를 안쓰면 어떤 문제가 생길까? 

서버가 IP 로만 요청을 인지하면 어느 도메인에 매핑해야 하는 요청인지 알 수 없다.

Location

3xx응답 결과, Location 헤더 필드가 있으면 Location 위치로 리다이렉트 요청한다. 

201의 경우, 생성된 리소스의 URI를 의미한다. 

Allow 

허용 가능한 HTTP 메서드를 적어준다. 실무에서 잘 안씀.

URL경로는 있는데 get, head, put 만 제공하는 경우, 405 코드와 Allow 필드를 응답에 포함해야 한다. 

Retry-After

유저가 다음 요청을 하기까지 기다려야 하는 시간.(날짜 또는 초 단위)

503 코드에서 사용한다. 서비스가 언제까지 불능인지 Retry-After헤더에 시간을 실어서 알려줄 수 있다. 


7. 인증 헤더 

Authorization 

클라이언트 인증 정보를 서버에 전달한다 

인증 매커니즘에 따라 Authorization헤더에 넣을 내용이 달라진다. ex) O-auth, etc 

WWW-Authenticate 

리소스 접근 시 필요한 인증 방법을 정의한다. 

401 오류 발생 시, 이 헤더를 넣어줘야 한다. realm=‘apps’, title=‘example’ 등의 형태를 정의한다. 

 


8. 쿠키  

쿠키를 안 쓰는 시나리오 

브라우저 : 사용자가 로그인 정보를 서버에 보낸다. 

서버 : 로그인 성공했다고 응답한다. 

브라우저 : '마이페이지' 를 요청한다.  

서버 : (쿠키가 없으니까 요청 주는 사람이 누군지 모름) 누구지? 싶어서 메인 페이지를 응답한다. 

서버가 로그인 사용자를 모르는 이유

HTTP는 무상태 프로토콜이기 때문이다. (Stateless)

클라이언트와 서버가 요청과 응답을 주고받으면 연결이 끊어진다. 

따라서 클라이언트와 서버는 서로 상태를 유지하지 않는다. 

쿠키를 포함한 요청이 아니라면 서버는 이 사람이 로그인 했는지 누군지 모른다. 

 

그렇다고 모든 요청에 사용자 정보를 포함한다면? 

-> 메뉴를 하나 이동 하더라도 사용자 개인정보를 포함하면, 보안 취약점이 많아진다. 

쿠키를 사용하는 시나리오 

브라우저가 모든 요청에 쿠키를 자동으로 포함하는 방식을 생각해보자. 

로그인 성공 시, 서버는 set-cookie: user=홍길동  이라는 쿠키를 응답해준다. 

서버의 응답을 받은 브라우저는 쿠키 저장소(브라우저 내부)에 이 쿠키를 저장한다. 

브라우저는 요청을 보낼 때 마다 쿠키저장소에서 쿠키를 뒤지고, 있으면 쿠키를 넣어서 HTTP 요청을 보낸다

(하지만 이것도 문제점이 있는 방법이다.)

쿠키의 형태

set-cookie: sessionid=abcd123; expires= 26-dec-2021 path=/; domain=google.com

쿠키의 용도

1) 사용자 로그인 세션 관리 
2) 광고 정보 트래킹 

[유의점] 쿠키 정보는 항상 서버에 전송된다!
네트워크 트래픽 추가 유발한다. 그래서 최소한의 정보만 사용해야 한다. (세션 id, 인증 토큰)
서버에 전송하지 않고, 웹 브라우저 내부에 데이터를 저장하고 싶으면 웹스토리지 참고. (localStorage, sessionStorage) 

[주의점]
보안에 민감한 데이터는 저장하면 안됨(주민번호, 신용카드 번호 등)

쿠키의 생명주기

expries : 만료일이 되면 쿠기가 자동으로 삭제되게 지정 
max-age : 0이나 음수를 지정하면 쿠키 삭제 

쿠키의 종류 2가지

세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시까지 유지 
영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지 

쿠키 도메인

특정 도메인을 명시하면 : 특정 도메인과 서브 도메인을 포함한 요청 시 해당 쿠키를 보낸다. 
만약 도메인을 생략하면 : 현재 문서 기준 도메인만 적용한다. 

쿠키 경로

이 경로를 포함한 하위 경로 페이지만 쿠키를 접근한다. 
일반적으로 path=/ 루트로 지정한다. 
하나의 도메인 내부의 ‘모든 경로’에서 쿠키를 쓰는 것이 일반적이기 때문이다. 

쿠키와 관련된 보안3가지

1) Secure

Secure를 넣으면 https인 경우에만 쿠키를 서버로 전송한다. 

 

2) HttpOnly

XSS 공격 방지 

자바스크립트에서 쿠키에 접근 불가하도록 제한한다.  
HTTP 전송에만 사용한다. 

 

3) SameSite

XSRF 공격 방지 
요청 도메인과 쿠키에 설정된 도메인이 같은 경우에만 쿠키를 서버로 전송한다. 
SameSite는 가장 최근에 나온 것이라서 브라우저의 지원 여부를 확인하고 사용하자. 


김영한님의 모든 개발자를 위한 HTTP 웹 기본 지식을 공부하고 요약했습니다. 

728x90

'프로그래밍 > HTTP' 카테고리의 다른 글

HTTP 헤더 - 일반 헤더(1)  (0) 2021.12.14
HTTP 4xx 클라이언트 오류 5xx 서버 오류  (0) 2021.12.14
HTTP 리다이렉션  (0) 2021.12.14

+ Recent posts