투 포인터

투 포인터는 2개의 포인터로 문제를 푸는 것으로 즉, 가르키는 값을 2개로 둬서 문제를 푸는 방법이다. 

투 포인터는 2개의 값을 통해 나올 수 있는 경우의 수와 기준 값 즉 문제에서 요구하는 바에 따라 이동원칙을 만들고 이에 따라 코드로 구현하는 것이 핵심이다. 

예시 1. 좋은 수 구하기

 

이 문제는 수를 입력받아 배열을 만들고 정렬한 후 양쪽 끝에 투 포인터 i,j를 위치시키고 이동원칙을 만든 후 탐색을 수행하면 된다.

좋은 수를 K라고 했을 때 K이면 좋은 수이고 이것을 구하는 이동원칙을 구하면 된다. 

이동원칙(가정 i<j )

1. A[i]+A[j] >K  -> j-- 2. A[i]+A[j}<K i++ 3. A[i]+A[j] ==k count++ break; 

또한 정렬된 데이터에서 좋은 수를 만들 떄, 자기자신은 포함 시키면 안된다.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	int N;
	cin >> N;
	vector<int>A(N, 0);

	for (int i = 0; i < N; i++) {
		cin >> A[i];
	}
	sort(A.begin(), A.end());
	int Result = 0;

	for (int k = 0; k < N; k++) {
		long find = A[k];
		int i = 0;
		int j = N - 1;
		while (i < j) {
			if (A[i] + A[j] == find) {
				if (i != k && j != k) {
					Result++;
					break;
				}
				else if (i == k) {
					i++;
				}
				else if (j == k) {
					j--;
				}
			}
			else if (A[i] + A[j] < find) {
				i++;
			}
			else {
				j--;
			}
		}

	}
	cout << Result << "\n";
}

 

※슬라이딩 윈도우

배열을 이동하면서 구한 부분 배열의 값을 업데이트 해주고 이에 따라 문제에서 주어진 조건을 확인하며 문제를 푸는 것이다. 

 

예시 문제 

최솟값찾기 1

이 문제에서는 부분 배열에서 최솟값을 구하는 것인데 최솟값을 구하는 범위가 i-L+1~ i 로 길이가 L인 부분 배열이다. 

이 때, n과 l의 최대 범위가 5,000,000이여서 O(nlogn)인 정렬을 사용할 수 가 없다. 그래서 이 문제에서는 슬라이딩 윈도우를 덱으로 구현하여 정렬효과를 볼 수 있다. 

deque<pair<int,int> mydeque; 라고 했을 때 숫자 값 , 인덱스 순서로 값을 넣어주고

현재값이 기존값보다 크다면 뒤에 넣어주고 만약 끝 인덱스- 주어진 부분배열 크기 했을 때, 처음 인덱스보다 크거나 같다면 처음값을 빼준다

현재값이 기존값보다 작다면 기존값을 삭제하고 넣어준다. 

이렇게 되면 덱에서 제일 첫번째에 있는 값이 부분배열의 최솟값이 된다.

#include <iostream>
#include <deque>
using namespace std;
typedef pair<int, int> Node;

int main() {
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	int N, L;
	cin >> N>>L;
	deque<Node> dq;

	for (int i = 0; i < N; i++) {
		int now;
		cin >> now;

		while (dq.size() && dq.back().first > now) {
			dq.pop_back();
		}
		dq.push_back(Node(now, i));

		if (dq.front().second <= i - L) {
			dq.pop_front();
		}
		cout << dq.front().first << ' ';
	}
	
	
}

코딩테스트 공부를 하다가 난이도가 올라갈 수록 알고리즘을 분석하고 여러유형에 대해 이론적으로 이해하고 있는 부분이 부족하다고 생각이 들어 알고리즘과 여러 유형에 대해 익혀보는  시간을 가져보려고 한다. 

1. 구간 합

구한 합의 핵심이론은 a~b 구간 사이의 합을 구하는 것이다. 이 때, 배열을 두고 직접 합을 구할 때, 최악의 경우 i가 0이고 j가 N인 경우 시간복잡도가 O(N)이 나와서 연산 수가 많아지면 시간초과가 뜰 수 있다. 구간 합의 경우 연산 수 가 많아지는 경우가 많기 때문에 배열 합을 쓰는게 좋다. 

 

*합 배열 

합 배열은 기존의 배열에서 구간마다의 합을 만들어두는 것으로 이 배열을 만들어 두고 이를 활용하여 정답을 구한다면  

연산수가 O(1)로 줄어들게 된다. 

 

-1차원에서의 합 배열

1차원 배열에서 합 배열을 만들 때 S가 합배열이고 B가 배열이라고 했을 때, S[i]=S[i-1] + B[i] 이다. 

그리고 만약 i~j까지의 구간 합을 구한다면 S[j] -S[i] 로 정답을 구할 수 있다.

-2차원에서의 합 배열

2차원 배열에서 합 배열을 만들 때는 경우의 수를 2가지 생각해주어야 한다

일단 1행일때 열이 변하는경우, 1열일때, 행이 변하는 경우를 생각해보자면 1차원에서의 합 배열을 구하는 공식과 똑같은데 

1행 일때 -> S[i][j] = S[1][j-1]+B[i][j] 

1열 일때 -> S[i][j] = S[i-1][i]+ B[i][j]

-> 전까지의 합 + 현재위치의 값

나머지 행과 열 일때

S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+B[i][j] 

-> i-1 값(이전 값의 행)에서 열 방향에서의 합 + j-1(이전 값의 열)값에서 행 방향  + 중복되게 더해진 값 - 현재 위치의 값

 

그리고 X1,Y1 ~ X2,Y2 까지의 구간 합을 구한다면

S[X2][Y2]-S[X1-1][Y2]-S[X2][Y1-1]+S[X1][Y1]

->X2,Y2까지의 구간합 - X1-1 값에서의 열 방향에서의 합 - Y1-1 값에서의 행 방향에서의 합 + 중복되게 빼진 값

 

예시 문제

나머지 합 구하기

이 문제에서 보면 N의 최대값이 10^6으로 최악의 경우 1초안에 연산하기는 힘들기 때문에 합 배열을 사용해야한다. 

※핵심 아이디어

1. (A+B) % C = ((A%C)+(B%C))%C -> 특정 구간 수를 나머지 연산해서 더한 값을 나머지 연산하는 것이나 구간합을 나머지 연산한 값이나 같다. 

2. if S[j]%M == S[i]%M 이라면 (S[j]-S[i])%M==0이다. 

 

흐름을 살펴보자면 

1. 합 배열을 만든다.

2. %M의 값으로 배열을 업데이트 해준다.

3. 경우의 수를 세어준다. (0일때는 0인 개수만큼 경우의 수를 추가, 2보다 클때에는 조합을 통해 경우의 수를 추가)

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	int N, M;
	cin >> N >> M;
	vector<long> S(N, 0);
	vector<long> C(N, 0);
	long answer = 0;
	cin >> S[0];

	for (int i = 1; i < N; i++) {
		int tmp = 0;
		cin >> tmp;
		S[i] = S[i - 1] + tmp;
	}
	for (int i = 0; i < N; i++) {
		int r = S[i] % M;
		if (r == 0) {
			answer++;
		}
		C[r]++;
	}
	for (int i = 0; i < M; i++) {
		if (C[i] > 1) {
			answer += (C[i] * (C[i] - 1) / 2);
		}
	}

	cout << answer << "\n";
}

 

 

무게에 따른 사람의 수를 모두 저장해준다음 그 벡터를 순회하면서

같은 무게가 있으면 어디에 앉든 균형을 이루기 때문에 조합으로 수를 구해주고

2 3 4 m 자리가 있어서 나올 수 있는 비율은 2:3 2:4=1:2 3:4 가 있기 때문에

이 에 맞는 값이 있다면 경우의 수 를 추가해주면 된다.

#include <string>
#include <vector>

using namespace std;

long long solution(vector<int> weights) {
    long long answer = 0;
    vector<long long> cnt(1001, 0);      //몸무게 별 사람수
    for (int i = 0; i < weights.size(); i++) {
        cnt[weights[i]]++;
    }
    //나올수 있는 비율 2:3 2:4=1:2 3:4
    for (int i = 0; i < weights.size(); i++) {
        if (weights[i] % 2 == 0) {                //2:3
            long long base = (weights[i] / 2) * 3;
            if (base <= 1000) answer += cnt[base];
        }
        if (weights[i] % 3 == 0) {                //3:4
            long long base = (weights[i] / 3) * 4;
            if (base <= 1000) answer += cnt[base];
        }
        long long base = weights[i] * 2;
        if (base <= 1000) answer += cnt[base];
    }

    for (int i = 100; i <= 1000; i++) {
        if (cnt[i] >= 2)
            answer += (long long)(cnt[i] * (cnt[i] - 1)) / 2;           //같을 때
    }

    return answer;
}

 

ext의 값이 val_ext보다 낮은 것을 저장하고 sort_by값에 따라 오름차순 정렬하는 문제이다. 처음에는 switch 문을 통해 idx1, idx2를 정해서 저장 및 정렬을 하려고 했는데 switch 문에는 문자열을 그냥 쓸 수 가 없는 것 같아서 map 자료형을 통해 idx 값을 정해서 저장 및 분류를  해주었다. 또한 compare 함수로 기준 값에 따라 오름차순 정렬되도록 만들었다.

 

코드

#include <string>
#include <vector>
#include <algorithm>
#include <map>

using namespace std;
map<string,int> m1={{"code",0},{"date",1},{"maximum",2},{"remain",3}};
int idx;
bool compare(const vector<int> &data1,const vector<int> &data2){
    return data1[idx]<data2[idx];
}
vector<vector<int>> solution(vector<vector<int>> data, string ext, int val_ext, string sort_by) {
    vector<vector<int>> answer;
    idx=m1[sort_by];
    for(auto d: data){
        if(d[m1[ext]]<=val_ext){
            answer.push_back(d);
        }
    }
    sort(answer.begin(),answer.end(),compare);
    return answer;
}

 

의사코드가 잘나와있는 문제라 그대로 구현만 해주면 쉽게 풀리는 문제이다. 중요한 건 위 아래 왼쪽 오른쪽을 탐색하고 그 탐색한 값이 표의 크기 안에서 존재하는 지 확인해주면 된다.

#include <string>
#include <vector>

using namespace std;

int dh[4]= {0,1,-1,0};
int dw[4]= {1,0,0,-1};
int solution(vector<vector<string>> board, int h, int w) {
    int answer = 0;
    int size=board.size();
    int wsize=board[1].size();
    for(int i=0;i<4;i++){
        int h_check=h+dh[i];
        int w_check=w+dw[i];
        if(h_check>=0&&h_check<size&&w_check>=0&&w_check<wsize){
            if(board[h][w]==board[h_check][w_check]){
                answer++;
            }
        }
    }
    return answer;
}

참가자 이름과 완주자이름을 비교하여 완주하지못한 사람을 찾으면 된다. 

나는 map 자료형을 사용해서 참여자 벡터를 순회하면서 해당 이름의 값을 증가시켜주고 그 뒤에 완주자 벡터를 순회하면서 해당 이름의 값을 줄여준다. 이때 해당 이름의 값이 1보다 크거나 같을 경우 통과하지 못하였다는 것이므로 answer에 추가해준다. 

동명이인이 있을 수 도 있어서 answer에 넣은 뒤 해당 이름의 값을 줄여준다. 

#include <string>
#include <vector>
#include <map>
using namespace std;
map<string,int> m1;
string solution(vector<string> participant, vector<string> completion) {
    string answer = "";
    for(int i=0;i<participant.size();i++){
        m1[participant[i]]++;
    }
    for(int i=0;i<completion.size();i++){
        m1[completion[i]]--;
    }
    for(int i=0;i<participant.size();i++){
        if(m1[participant[i]]>=1){
            answer+=participant[i];
            m1[participant[i]]--;
        }
    }
    return answer;
}

처음에는 전체를 1로 만들고 체육복을 도난당한 사람을 0으로 여분의 체육복이 있는 사람을 2로 한 뒤 앞의 사람을 먼저 체크하고 뒤를 체크하는 방식으로 풀려고 했는데 여분의 체육복이 있는 사람도 도난 당할 수 있다는 점을 제대로 못 봐서 틀렸다. 어떻게 하면 좋을까 생각해봤을 때 전체를 1로 만들고 여분이 있는 사람은 2로 만든 다음에 도난 당한 사람은 해당 값을 --로 빼주면 될 것 같다고 생각했다. 

#include <string>
#include <vector>
#include <map>
using namespace std;
map<int, int> m1;
int solution(int n, vector<int> lost, vector<int> reserve) {
    int answer = 0;
    for (int i = 1; i <= n; i++) {
        m1[i] = 1;					//전체를 1로 
    }

    for (int i = 0; i < reserve.size(); i++) {
        m1[reserve[i]] = 2;				//여분이 있는 사람은 2로
    }
    for (int i = 0; i < lost.size(); i++) {
        m1[lost[i]]--;					//도난당한 사람은 개수 빼주기
    }
    for (int i = 1; i <= n; i++) {
        if (m1[i] == 0) {				//만약 도난당해서 없는 사람이 있다면
            if (m1[i - 1] == 2) {		//앞사람을 먼저 체크
                m1[i - 1]--;
                m1[i]++;
            }
            else if (m1[i + 1] == 2) {
                m1[i + 1]--;
                m1[i]++;
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        if (m1[i] >= 1) {			//1개 이상이 있다면 수업을 들을 수 있다.
            answer++;
        }
    }
    return answer;
}

 

 

문제가 길지만 요약하자면

숫자- 점수를 받고

영어- 점수를 계산

* / # - 보너스 점수

이여서 string을 순회하면서 이 점을 체크해주면 된다. 

*일때는 해당 점수와 바로 전의 점수를 2배 해주면 되고 #일 때는 마이너스로 만들어 주면 되는데 이 것은 영어가 나올 때 * #이 있는 지 확인해서 있다면 적용해주는 방식으로 하면 된다.

#include <string>
#include <cmath>
using namespace std;

int solution(string dartResult) {
    int answer = 0;
    int prev=0,score=0;
    
    
    for(int i=0;i<dartResult.size();i++)
    {
        //숫자 분리
        if(dartResult[i]>='0' && dartResult[i]<='9')
        {
            prev=score;         //보너스 계산을 위해 
            
            if(dartResult[i+1]=='0')
            {
                score=10;
                i++;
            }else{
                score=dartResult[i]-'0';
            }
        }else if(dartResult[i]=='S' || dartResult[i]=='D' || dartResult[i]=='T')
        {
            if(dartResult[i]=='D'){
                score=pow(score,2);
            }else if(dartResult[i]=='T'){
                score=pow(score,3);
            }
            
            if(dartResult[i+1]=='*')
            {           //보너스 점수 계산
                answer-=prev;
                prev*=2;
                score*=2;
                i++;
                answer+=prev;
            }else if(dartResult[i+1]=='#'){
                score*=-1;
                i++;
            }
            answer+=score;
        }
        
    }
    return answer;
}

 

코딩 문제는 머리로는 어떻게 풀 지 생각이 되는데 그걸 언어로 구현을 생각하기 어렵다. 천천히 풀어 보았다. 

일단 중요한 것은 스테이지 별로 클리어 하지 못한 사용자 수를 저장해야하고

스테이지를 깨고 다음 스테이지에 간 사람도 알고 있어야한다. 

생각을 해보니 

일단 첫번째에 실패한 사람은 다음 스테이지는 당연히 실패하는 것이여서

전체 수를 체크해두고 뺴주는 식으로 가면 된다.

그리고 실패율과 스테이지는 vector 안에 pair 자료형을 통하여 저장하고 정렬은 정렬함수를 만들어서 정렬을 할 수 있게 한다. 

#include <string>
#include <vector>
#include <algorithm>
using namespace std;
bool compare(pair<double,int> &a,pair<double,int> &b){
    if(a.first==b.first) return a.second < b.second;            //실패율 같다면 숫자 큰 순으로
    else return a.first > b.first;
}

int cnt[501];            //스테이지별 클리어하지 못한 사용자 수
vector<pair<double,int>> failure;           //스테이별 실패율 저장할 벡터
vector<int> solution(int N, vector<int> stages) {
    vector<int> answer;
    
    int st= stages.size();
    for(int i=0;i<st;i++){
        cnt[stages[i]]++;           //스테이지별 클리어하지 못한 사용자 수 저장
    }
    int tmp=st;
    for(int i=1;i<=N;i++){
        
        if(cnt[i]==0) failure.push_back({0,i});     //실패한 사람이 없다면 실패율은 0
        else{
            double t=(double)cnt[i]/tmp;             //실패율 계산
            
            failure.push_back({t,i});
        }
        
        tmp-=cnt[i];         //해당단계에서 실패했다면 다음단계도 실패 총 인원 줄이기
    }
    sort(failure.begin(),failure.end(),compare);
    
    int fsize=failure.size();
    
    for(int i=0;i<fsize;i++){
        auto p =failure[i];
        answer.push_back(p.second);
    }
    
    return answer;
}

숫자 n 까지의 소수를 찾는 문제이다. 처음에는 n까지 for 문을 돌면서 소수를 판별하는 함수로 개수를 세려고 했는데 이렇게 해보니

 효율성 테스트에서 시간초과가 떴다. 왜 그런지 생각해보니 for문을 돌면서 계속 검사를 하면 O(N^2)로 시간복잡도가 계산되기 때문에 실패하는 것이였다. 

방법을 찾아보니 에라토스테네스의 체 라는 방법을 사용하면 시간을 많이 줄일 수 있다고 해서 이 방법을 익히고 사용해 보기로 하였다. 

에라토스테네스의 체

https://ko.wikipedia.org/wiki/%EC%97%90%EB%9D%BC%ED%86%A0%EC%8A%A4%ED%85%8C%EB%84%A4%EC%8A%A4%EC%9D%98_%EC%B2%B4

에라토스테네스의 체를 설명한 그림이다. 

소수 인것을 찾고 소수의 배수를 찾아 체크하는 과정을 반복하면 체크하지 않은 것이 소수이여서 이런 방식으로 소수를 찾을 수 있다.

전체 코드

#include <string>
#include <vector>

using namespace std;

int solution(int n) {
    int answer = 0;
    vector<bool> v(n+1,true);
    
    for(int i=2;i<=n;i++){              //전체 순회
        if(v[i]){               //소수라면
            for(int j=2;j*i<=n;j++){            //배수 찾기
                v[i*j]=false;               //배수는 소수가 아니다.
            }
            answer++;
        }
    }
    return answer;
}

+ Recent posts