본문 바로가기
알고리즘

99클럽 코테 스터디 1일차 TIL(두 큐 합 같게 만들기 /프로그래머스)

by 순원이 2024. 3. 26.

문제

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

문제 설명

 

요구사항 정리

  • 각 큐의 합이 같도록 하는 최소 횟수
  • 넣고 빼는데 '큐라는' 자료구조 특성을 반영해야 함
  • 각 큐의 원소 합을 같게 만들지 못한다면 -1을 리턴

해결법

  • 두 개를 1/2 씩 맞춘다는 관점보다 하나를 1/2 맞추면 다른 하나는 1/2가 맞춰진다로 생각
  • 먼저 두 큐 원소의 합을 저장하는 변수 생성
  • 최소 횟수이려면 큐 앞단에 위치한 요소들을 움직여야 함
  • A큐의 원소 하나를 빼서 B큐에 넣으면 B큐의 원소 하나도 A큐로 가야함(길이를 맞춰야 하기 때문에)
  • A큐 < - > B큐의 원소가 모두 바뀌었을 때도 1/2를 못 맞춘다면 방법이 없으므로 -1 리턴
  • 한 큐의 원소를 한 번만 탐색하면 되므로 O(n)

 

1 번째 코드

class Solution {
    public int solution(int[] queue1, int[] queue2) {
        
        long queue1Sum = 0;
        long queue2Sum = 0;
        long allSum =0;
        int result = -1;
        
        for(int i =0; i < queue1.length; i++) {
            queue1Sum += queue1[i];
            queue2Sum += queue2[i];
        }
        
        allSum += queue1Sum + queue2Sum;
        
        //큐1이랑 큐2 원소 바꿨을 때 큐1의 합이 1/2인지 확인
        for(int i= 0; i < queue1.length; i++) {
            queue1Sum -= queue1[i];
            queue1Sum += queue2[i] ;  
            
            if(queue1Sum == allSum/2){
                result = i;
                break;
            }    
        }
        
        return result;
    }
}

 

문제

테스트 하나를 통과했지만 두 개를 통과하지 못했다.
즉, 예외상황을 놓쳤거나 로직이 틀렸다는 것이다.

테스트1 실행결과에서 알 수 있는 것
for 문으로 큐1을 순회할 때 그에 해당하는 인덱스 i를  result로 주입했다.
큐1의 원소가 하나 빠진다는 건 큐2의 원소도 하나가 빠진다는 것이기 때문에 i*2가 result이다.

테스트2 실행결과에서 알 수 있는 것

정답이 7로 홀수이다. 문제를 잘못이해했다. 최종 큐1과 큐2의 길이가 같아야 하는줄 알았다.
그럼, 테스트1 실행결과에서 알 수 있는 것도 소용이 없게된다. 해결법을 수정하겠다.

 

해결법

  • 두 개를 1/2 씩 맞춘다는 관점보다 하나를 1/2 맞추면 다른 하나는 1/2가 맞춰진다로 생각
  • 먼저 두 큐 원소의 합을 저장하는 변수 생성
  • 최소 횟수이려면 큐 앞단에 위치한 요소들을 움직여야 함
  • A큐의 원소 하나를 빼서 B큐에 넣으면 B큐의 원소 하나도 A큐로 가야함(길이를 맞춰야 하기 때문에)
  • A큐 < - > B큐의 원소가 모두 바뀌었을 때도 1/2를 못 맞춘다면 방법이 없으므로 -1 리턴
  • 한 큐의 원소를 한 번만 탐색하면 되므로 O(n)

+

  • 큐1이 1/2보다 얼마나 부족한지 넘치는지 계산한다.
    • 큐1이 1/2보다 부족하다면 큐2에서 빼온다
    • 또 부족하면  큐2에서 또 뻬온다 넘친다면 넘겨준다

ex)

[1,2,1,2] [ 1,10,1,2] - 0번째

[1,2,1,2,1,] [10,1,2]  7 13 -1번째    

[1,2,1,2,1,10] [1,2]  17 3 - 2번째    

[2,1,2,1,10] [1,2,1]  16 4 - 3번째

[1,2,1,10] [1,2,1,2]  14  6 - 4번째

[2,1,10] [1,2,1,2,1]  13  7 - 5번째

[1,10] [1,2,1,2,1,2]  11  9 - 6번째

[10] [1,2,1,2,1,2,1]  10  10  - 7번째

 

  • 큐1이 원소를 가지는 경우의 수(현재 상태 제외)는 어떻게 될까?
    • {1,2,3,}  {4}
    • {1,2,3,4} {}
    • {2,3,4} {1}
    • {3,4} {1,2}
    • {4} {1,2,3}
    • {} {1,2,3,4}
    • 큐2의 원소 수 + (큐1의 원소수 -1) 만큼 만 탐색해보고 없으면 -1를 리턴하면 되겠다

 

 

2 번째 시도

import java.util.*;
import java.util.stream.*;

class Solution {
    public int solution(int[] queue1, int[] queue2) {
        
        long queue1Sum = 0;
        long queue2Sum = 0;
        long allSum =0;
        long halfSum = 0;
        int result = 0;
        List<Integer> q1 = Arrays.stream(queue1).boxed().collect(Collectors.toList());
        List<Integer> q2 = Arrays.stream(queue2).boxed().collect(Collectors.toList());
        
        for(int i =0; i < q1.size(); i++) {
            queue1Sum += q1.get(i);
            queue2Sum += q2.get(i);
        }
        
        allSum += queue1Sum + queue2Sum;
        halfSum = allSum/2;
        
        //큐1이 1/2 인지 확인
        int queue1Index = 0 ;
        int queue2Index = 0 ;
        int i =0;
        do{
            if(queue1Sum > halfSum) {
                queue1Sum -= q1.get(0);
                q2.add(q1.get(0));
                q1.remove(0);
            }
            else if(queue1Sum < halfSum ) {
                queue1Sum += q2.get(0);
                q1.add(q2.get(0));
                q2.remove(0);
            }
            i++;
            result++;
            if(i > (q1.size() + q2.size() -1)){
                    result = -1;
                    break;
            }
        }while(queue1Sum != halfSum);
        
        return result;
    }
}

문제점

효율성 문제: List에서 요소를 제거하거나 추가할 때의 시간 복잡도는 리스트의 구현에 따라 다를 수 있지만, 일반적으로 ArrayList를 사용할 경우 요소를 제거하거나 추가하는 데 O(n)의 시간이 소요됩니다. 이는 리스트의 크기가 커질수록 성능 문제를 일으킬 수 있습니다. 특히, 요소를 리스트의 시작 부분에서 추가하거나 제거할 때 나머지 모든 요소들을 이동해야 하기 때문에 비효율적입니다.

 

해결법

  • 자료구조 변경: ArrayList -> Queue로 변경(LinkedList로 구현, 수정 삭제에 좋음)

                        or

  • 알고리즘 변경: 그리드 ->  슬라이딩 원도우
    • 두 큐의 요소를 하나의 순환 큐로 간주하고, 요소의 이동 없이 요소의 합만을 계산하여 목표 합에 도달할 수 있는지 확인할 수 있습니다
    • ex) {1,2}  {3,4} 두 개의 큐가 있다면 {1,2,3,4} 큐 하나로 보기 

 

3번째 시도(그리드)

import java.util.*;
class Solution {
    public long getsum(int[] q) {
        long sum = 0;
        for(int i = 0; i < q.length; i++) 
        {
            sum += (long) q[i];
        }
        return sum;
    }
    public int solution(int[] queue1, int[] queue2) {
        int answer = 0;
        long sum1 = getsum(queue1);
        long sum2 = getsum(queue2);
        Queue<Integer> q1 = new LinkedList<>();
        Queue<Integer> q2 = new LinkedList<>();        
        for(int i = 0; i < queue1.length; i++) {
            q1.add(queue1[i]);            
            q2.add(queue2[i]);
        }
        while(sum1 != sum2) {
            if(answer > (queue1.length + queue2.length) * 2)
                return -1;
            int val = 0;
            if(sum1 < sum2) {
                val = q2.poll();
                q1.add(val);
                sum1 += (long) val;
                sum2 -= (long) val;
            }
            else if(sum1 > sum2) {
                val = q1.poll();
                q2.add(val);
                sum1 -= (long) val;
                sum2 += (long) val;
            }
            else 
            {
                return answer;
            }
            answer++;
        }
        return answer;
    }
}

출처: https://velog.io/@qodlstjd12/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%91%90-%ED%81%90-%ED%95%A9-%EA%B0%99%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0-Java

 

2 번째 코드와 자료구조만 바뀐 것이므로 설명은 생략하겠습니다

 

3 번째 시도(슬라이딩 윈도우)

import java.util.*;

class Solution {
    public int solution(int[] queue1, int[] queue2) {
        int n = queue1.length;
        int[] a = new int[2*n];
        
        for(int i = 0; i < n; i++){
            a[i] = queue1[i];
            a[n+i] = queue2[i];
        } //새로운 배열 초기화
        
        long target = 0;
        for(int i = 0; i < 2*n; i++)
            target += a[i];
        if(target % 2 != 0) return -1;
        target /= 2; // 합이 target이 되는 구간을 찾아야 함
        
        int ans = 0x7f7f7f7f; // 일단 매우 큰 값을 넣어놓고 시작
        int en = 0;
        long tot = a[0]; // 현재 보는 구간의 합 = a[st]+...+a[en], 범위가 양 끝을 포함(inclusive)한다는 점과 en < st일 땐 a[st]+...+a[2*n-1]+a[0]+...+a[en]을 나타냄에 주의
        
        for(int st = 0; st < 2*n; st++){
            while(tot < target){
                en = (en + 1) % (2*n);
                tot += a[en];
            }
            // while문을 탈출하면 a[st]+...+a[en] 구간의 합이 target 이상임을 의미
            if(tot == target){ // 구간의 합이 정확히 target과 일치할 경우
                int moves; // 작업 횟수
                if(en < n-1) moves = 3*n + 1 + st + en; // 3n+1번의 연산을 통해 큐1에 첫 번째 원소만 남길 수 있고, 이후 st+en 번의 추가적인 연산을 거쳐 a[st] .. a[en]을 큐1에 둘 수 있음
                else moves = st + (en - n + 1);
                ans = Math.min(ans, moves);
            }
            tot -= a[st];
        }
        if(ans == 0x7f7f7f7f) ans = -1;
        return ans;
    }
}

출처: https://github.com/encrypted-def/kakao-blind-recruitment/blob/master/2022-summer-internship/Q2_1.java

 

1단계: 두 개의 대기열을 하나의 배열로 결합

두 개의 대기열이 있다고 가정

  • queue1 = [3, 2, 7, 2]
  • queue2 = [4, 6, 5, 1]

이를 하나의 배열로 결합하고 쉽게 처리할 수 있도록 원형 배열로 처리합니다.

  • a = [3, 2, 7, 2, 4, 6, 5, 1]

이 배열 'a'는 두 대기열을 연속적으로 나타냅니다.

2단계: 목표 합계 계산

a에 있는 모든 요소의 총합을 계산하고 2로 나누어 각 대기열이 이상적으로 도달해야 하는 목표 합을 찾습니다.

  • 총합 = 30
  • 목표 합계 = 15

3단계: 슬라이딩 윈도우 검색

슬라이딩 윈도우 접근 방식을 사용하여 이 원형 배열 내에서 합계가 목표 합계와 동일한 연속 세그먼트를 찾습니다. 첫 번째 요소에서 시작하도록 창을 초기화합니다.

  • st = 0과 en = 0으로 시작하고 첫 번째 요소의 총합 tot = a[0].

슬라이딩 윈도우 시각화

이 창을 a 배열 위로 이동하는 것을 시각화해 보겠습니다.

  • 처음에는 창에 첫 번째 요소 [3]만 포함됩니다. 총계(tot)가 목표보다 작으므로 창을 확장합니다.
  • 총계(tot)가 목표와 같거나 초과할 때까지 더 많은 요소를 포함하도록 창을 확장하세요. 예를 들어 창이 확장되어 [3, 2, 7, 2]를 포함하는 경우 총계는 이제 14로 여전히 목표보다 낮습니다. 계속 확장합니다..
  • 창에 '[3, 2, 7, 2, 4]'가 포함되면 총계는 '18'로 목표를 초과합니다. 이제 처음부터 창을 축소하여 대상에 더 가까워지려고 합니다.
  • 대상과 정확히 일치하는 창(예: [7, 2, 4, 2])을 찾으면 원래 대기열에서 이 배열을 달성하는 데 필요한 작업을 계산합니다.

4단계: 동작 계산 및 답변 업데이트

목표 합계와 일치하는 모든 창에 대해 필요한 최소 이동 횟수를 계산합니다.

  • 창이 [7, 2, 4, 2](단지 예)인 경우 원래 대기열에서 시작하여 이 배열을 가져오는 데 필요한 이동 횟수를 결정합니다. 여기에는 한 큐에서 요소를 꺼내서 다른 큐에 추가하는 작업이 포함됩니다.

마지막 단계: 최소 이동 횟수 반환

가능한 모든 창을 조사한 후 균형 잡힌 대기열을 달성하는 데 필요한 최소 이동 수를 반환합니다. 그러한 배열이 가능하지 않은 경우(즉, 어떤 세그먼트에서도 목표 합계를 달성할 수 없는 경우) -1을 반환합니다.

이 접근 방식은 총 합계 계산, 원형 배열 조작 및 슬라이딩 윈도우 기술의 조합을 활용하여 최소한의 작업으로 두 대기열의 균형을 맞추는 가능한 모든 방법을 체계적으로 탐색하여 솔루션을 효율적으로 찾습니다.

 

얻은 점

  • Queue자료구조 깊이 공부
  • List 구현체 학습
  • ArrayList < - > array 변환법 공부
    • ArraysList<> = Arrays.asList(int[]) 이거 안됨
    • Stream.boxed().collect(Collector.toList())로 List 변환 / ArrayList로 변환 안됨
  • 슬라이딩 윈도우/투포인터 알고리즘 지식 획득
  • 알고리즘 문제 풀 때 제한사항 확인하여 시간복잡도 상한선 정하기