4월 17, 2024

[백준] 16638번 괄호 추가하기 2 비트마스크로 풀어보기

1. 문제

1) 링크

www.acmicpc.net/problem/16638

2) 문제

길이가 N인 수식이 있다. 수식은 0보다 크거나 같고, 9보다 작거나 같은 정수와 연산자(+, -, ×)로 이루어져 있다. 곱하기의 연산자 우선순위가 더하기와 빼기보다 높기 때문에, 곱하기를 먼저 계산 해야 한다. 수식을 계산할 때는 왼쪽에서부터 순서대로 계산해야 한다. 예를 들어, 3+8×7-9×2의 결과는 41이다.

수식에 괄호를 추가하면, 괄호 안에 들어있는 식은 먼저 계산해야 한다. 단, 괄호 안에는 연산자가 하나만 들어 있어야 한다. 예를 들어, 3+8×7-9×2에 괄호를 (3+8)×7-(9×2)와 같이 추가했으면, 식의 결과는 59가 된다. 하지만, 중첩된 괄호는 사용할 수 없다. 즉, 3+((8×7)-9)×2, 3+((8×7)-(9×2))은 모두 괄호 안에 괄호가 있기 때문에, 올바른 식이 아니다.

수식이 주어졌을 때, 괄호를 적절히 추가해 만들 수 있는 식의 결과의 최댓값을 구하는 프로그램을 작성하시오. 추가하는 괄호 개수의 제한은 없으며, 추가하지 않아도 된다.

3) 입력

첫째 줄에 수식의 길이 N(1 ≤ N ≤ 19)가 주어진다. 둘째 줄에는 수식이 주어진다. 수식에 포함된 정수는 모두 0보다 크거나 같고, 9보다 작거나 같다. 문자열은 정수로 시작하고, 연산자와 정수가 번갈아가면서 나온다. 연산자는 +, -, * 중 하나이다. 여기서 *는 곱하기 연산을 나타내는 × 연산이다. 항상 올바른 수식만 주어지기 때문에, N은 홀수이다.

4) 출력

첫째 줄에 괄호를 적절히 추가해서 얻을 수 있는 결과의 최댓값을 출력한다. 정답은 231보다 작고, -231보다 크다.


2. 풀이

더 자세한 입출력 예시는 위 백준 링크에서 확인할 수 있다. 

이 문제는 먼저 Class를 하나 더 만들어주는 것이 편하다. class Calc를 하나 만들어주고 instance로 num과 op를 가지고 있도록 만들어준다. 여기서 op는 operator의 약자로 숫자면 0, 더하기면 1, 빼기면 2, 곱하기면 3을 가지게 만들어준다. 

 

즉 아래와 같은 형태인 것이다.

class Calc{
    int num, op;
    Calc(int num, int op) {
        this.num = num;
        this.op = op;
    }
}

그런 다음에 비트마스크를 활용하여 괄호가 올 수 있는 모든 경우를 체크할 것인데, 이 문제는 괄호 안에 하나의 연산자밖에 존재하지 않고 중첩이 불가능하므로 오히려 쉬운 문제이다. 연산자의 개수는 (n-1)/2개이므로 연산자의 개수를 기준으로 비트마스크를 해주면 된다. 즉 for문의 형태가 아래와 같은 식인 것이다.

int m = (n-1)/2; //연산자의 개수
for(int i=0; i<(1<<m); i++){
            boolean possible = true;
            for (int j=0; j<m-1; j++) {
                if ((i&(1<<j)) > 0 && (i&(1<<(j+1))) > 0) {
                    possible = false; //중첩 괄호 확인
                }
            }
            if (!possible) continue;
            
            }

이런식으로 for문이 돌면 중첩괄호가 아닌 모든 괄호의 경우를 체크할 수 있고 이제는 괄호가 있는 경우를 먼저 계산해준다. 이 문제는 순서가 괄호가 있는 수 먼저 계산 -> 곱하기 먼저 계산 -> 나머지 계산 이런 식으로 진행되어야 한다. 

 

괄호를 먼저 계산하면, 원래 있는 수 배열이 훼손될 수 있기 때문에 tmp라는 새로운 배열을 하나 더 만들어주고 괄호를 계산해준다. 아래 코드는 괄호를 계산하는 부분의 코드이다.

Calc[] tmp=new Calc[n]; //tmp 배열에 옮기기
            for (int j=0; j<n; j++) {
                tmp[j] = new Calc(a[j].num, a[j].op);
            }
            for(int j=0; j<m; j++){
                if ((i&(1<<j))>0){ //괄호가 있으면 
                    int k=2*j+1; //실제 괄호의 위치
                     if (tmp[k].op == 1) { //더하기
                         tmp[k-1].num += tmp[k+1].num;
                        tmp[k].op = -1;
                        tmp[k+1].num = 0;
                    } else if (tmp[k].op == 2) { //빼기
                        tmp[k-1].num -= tmp[k+1].num;
                        tmp[k].op = -1;
                        tmp[k+1].num = 0;
                    } else if (tmp[k].op == 3) { //곱하기
                        tmp[k-1].num *= tmp[k+1].num;
                        tmp[k].op = -1;
                        tmp[k+1].num = 0;
                    }
                }
            }

다음에 *, +, -을 더 계산해야 하기 때문에 괄호로 이미 계산한 연산자의 경우 op의 값으로 -1을 가지게 업데이트 시켜주어 다음번 계산 시에 고려하지 않게 한다. 

 

이렇게 되었다면 괄호 부분의 숫자가 다 계산이 된 것이다. 이제 곱하기 부분을 먼저 계산해주고, 그 다음에는 순차적으로 하나씩 계산해주어서 최댓값을 찾아주면 된다. 

 


3. 코드

이 모든 것을 종합한 전체 Java code는 아래와 같다.

import java.util.*;
class Calc{
    int num, op;
    Calc(int num, int op) {
        this.num = num;
        this.op = op;
    }
}
public class Main{
    public static void main(String[] args){
          Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        String s = sc.next();
        Calc[] a = new Calc[n];
         for (int i=0; i<n; i++) {
            if (i%2 == 0) {
                a[i] = new Calc(s.charAt(i)-'0', 0);
            } else {
                int op = 1; //+일 경우
                if (s.charAt(i) == '-') {
                    op = 2;
                } else if (s.charAt(i) == '*') {
                    op = 3;
                }
                a[i] = new Calc(0, op);
            }
        }
        int m = (n-1)/2; //연산자의 개수
        int ans = -2147483648; //가장 최소값
        for(int i=0; i<(1<<m); i++){
            boolean possible = true;
            for (int j=0; j<m-1; j++) {
                if ((i&(1<<j)) > 0 && (i&(1<<(j+1))) > 0) {
                    possible = false; //중첩 괄호 확인
                }
            }
            if (!possible) continue;
            Calc[] tmp=new Calc[n]; //tmp 배열에 옮기기
            for (int j=0; j<n; j++) {
                tmp[j] = new Calc(a[j].num, a[j].op);
            }
            for(int j=0; j<m; j++){
                if ((i&(1<<j))>0){ //괄호가 있으면 
                    int k=2*j+1; //실제 괄호의 위치
                     if (tmp[k].op == 1) { //더하기
                         tmp[k-1].num += tmp[k+1].num;
                        tmp[k].op = -1;
                        tmp[k+1].num = 0;
                    } else if (tmp[k].op == 2) { //빼기
                        tmp[k-1].num -= tmp[k+1].num;
                        tmp[k].op = -1;
                        tmp[k+1].num = 0;
                    } else if (tmp[k].op == 3) { //곱하기
                        tmp[k-1].num *= tmp[k+1].num;
                        tmp[k].op = -1;
                        tmp[k+1].num = 0;
                    }
                }
            }
            //괄호 계산 완료
            ArrayList<Calc> c=new ArrayList<>();
            for(int j=0; j<n; j++){
                if (j%2==0){ //숫자일 경우
                    c.add(tmp[j]);
                }else if (tmp[j].op==-1){
                 j++; //이미 괄호로 처리한 것
                }
                    else{
                    //우선 곱하기만 먼저 계산
                    if (tmp[j].op==3){
                        int num=c.get(c.size()-1).num* tmp[j+1].num;
                        c.remove(c.size()-1);
                        c.add(new Calc(num, 0));
                        j += 1;
                    }
                        else{
                            c.add(tmp[j]);
                        }
                }
            }
            Calc b[] = c.toArray(new Calc[c.size()]);
            int m2 = (b.length-1)/2;
            int val = b[0].num;
            for (int j=0; j<m2; j++) {
                int k = 2*j+1;
                if (b[k].op == 1) {
                    val += b[k+1].num;
                } else if (b[k].op == 2) {
                    val -= b[k+1].num;
                } else if (b[k].op == 3) {
                    val *= b[k+1].num;
                }
            }
            if (ans < val) {
                ans = val;
            }
        }
          System.out.println(ans);
    }
}

조금 긴 코드이지만 하나씩 이해해보면 어려움이 없을 것이다. 


2월 24, 2024

[백준] 1182번 부분집합 비트마스크로 풀어보기

1. 백준 문제 소개

1) 링크

www.acmicpc.net/problem/1182

2) 문제

N개의 정수로 이루어진 수열이 있을 때, 크기가 양수인 부분수열 중에서 그 수열의 원소를 다 더한 값이 S가 되는 경우의 수를 구하는 프로그램을 작성하시오.

3) 입력

첫째 줄에 정수의 개수를 나타내는 N과 정수 S가 주어진다. (1 ≤ N ≤ 20, |S| ≤ 1,000,000) 둘째 줄에 N개의 정수가 빈 칸을 사이에 두고 주어진다. 주어지는 정수의 절댓값은 100,000을 넘지 않는다.

4) 출력

첫째 줄에 합이 S가 되는 부분수열의 개수를 출력한다.

 

더 자세한 문제 제한을 보기 위해서는 위 링크를 타고 들어가보자.

 

2. 풀이

비트마스크란 비트연산을 사용하여 정수로 집합을 나타내는 것이다.

 

예를 들어 {1,3,4,5,9} 가 사용이 되었다면 우리는 이를 정수로 01000111010 이라고 표현할 수 있다. (binary digit이 0자리부터 시작한다고 생각했을 때, 1번째 자리, 3번째자리, 4번째 자리, 5번째 자리, 9 번째 자리를 1로 표시하는 것이다)

 

그러면 이 문제도 비트마스크로 풀어보자. 

 

각각의 숫자를 입력받고, 그 숫자가 부분집합에 포함되면 해당 자리수가 1, 포함되지 않으면 해당 자리수를 0이라고 생각하는 것이다. 

 

먼저 비트마스크를 처리한 코드부분만 살펴보자.

 

 for(int i=1; i<(1<<n); i++){
            int sum=0;
            for(int k=0; k<n; k++){
                if ((i&(1<<k))!=0){
                    sum+=a[k];
                }
            }
            if (sum==s){
                ans++;
            }
        }

이렇게 구할 수 있는데 for문을 1부터 1<<n전까지 처리한 이유에 대해서 생각해보자.

우선 0부터 처리하지 않은 이유는 공집합을 제외한다고 문제에서 나와 있기 때문이다.

그리고 비트마스크로 n자리를 처리하면 0부터 (1<<n)-1자리까지 가능하다. 총 n개의 숫자에 대해서 0부터 n-1자리가 모두 다 부분집합에 포함된다면 각 자리수가 모두 1이 되어서 (1<<n)-1라는 최댓값이 나온다.

 

따라서 가능한 모든 경우에 대해서 inner for문이 0부터 n-1자리까지 돌면서 해당 자리의 수가 1이라면 부분집합에 포함되었다는 말이기 때문에 이를 sum에다가 추가한다.

 

최종적으로 sum이 문제에서 주어진 합과 같다면 답을 찾은 것이므로, 답의 개수에 1을 더해주게 된다.  

import java.util.*;

public class Main{
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int s=sc.nextInt();
        int [] a=new int [n];
        for(int i=0; i<n; i++){
            a[i]=sc.nextInt();
        }
        int ans=0;
        for(int i=1; i<(1<<n); i++){
            int sum=0;
            for(int k=0; k<n; k++){
                if ((i&(1<<k))!=0){
                    sum+=a[k];
                }
            }
            if (sum==s){
                ans++;
            }
        }
        System.out.println(ans);
    }
}

종합적으로 코드를 살펴보면 위와 같이 구할 수 있다. 


2월 20, 2024

[백준] 14391번 종이조각 비트마스크로 풀어보기

 www.acmicpc.net/problem/14391

문제

영선이는 숫자가 쓰여 있는 직사각형 종이를 가지고 있다. 종이는 1×1 크기의 정사각형 칸으로 나누어져 있고, 숫자는 각 칸에 하나씩 쓰여 있다. 행은 위에서부터 아래까지 번호가 매겨져 있고, 열은 왼쪽부터 오른쪽까지 번호가 매겨져 있다.

영선이는 직사각형을 겹치지 않는 조각으로 자르려고 한다. 각 조각은 크기가 세로나 가로 크기가 1인 직사각형 모양이다. 길이가 N인 조각은 N자리 수로 나타낼 수 있다. 가로 조각은 왼쪽부터 오른쪽까지 수를 이어 붙인 것이고, 세로 조각은 위에서부터 아래까지 수를 이어붙인 것이다.

 

입력

첫째 줄에 종이 조각의 세로 크기 N과 가로 크기 M이 주어진다. (1 ≤ N, M ≤ 4)

둘째 줄부터 종이 조각이 주어진다. 각 칸에 쓰여 있는 숫자는 0부터 9까지 중 하나이다.

출력

영선이가 얻을 수 있는 점수의 최댓값을 출력한다.

 

예제와 제한이 궁금하다면 위의 링크를 클릭해 자세한 사항을 알아보자. 

 


이 문제는 비트마스크로 풀 수 있다. 

비트마스크란 비트연산을 사용하여 정수로 집합을 나타내는 것이다.

 

예를 들어 {1,3,4,5,9} 가 사용이 되었다면 우리는 이를 정수로 01000111010 이라고 표현할 수 있다. (binary digit이 0자리부터 시작한다고 생각했을 때, 1번째 자리, 3번째자리, 4번째 자리, 5번째 자리, 9 번째 자리를 1로 표시하는 것이다)

 

이 경우에는 n*m 의 칸이 있으니 각각을 자릿수로 하는 n*m-1자리 정수를 만들 수 있는 것이다.

 

각각의 칸에 대해서 가로로 묶을 것인지, 세로로 묶을 것인지 정하면 되는데 양자택일의 문제이므로

가로로 묶을 경우 해당 자릿수의 숫자를 0으로, 세로로 묶을 경우 해당 자릿수의 숫자를 1로 정했다고 가정하겠다.

 


 int sum=0;
 //가로 찾기
            for(int i=0; i<n; i++){
                int current=0;
                for(int j=0; j<m; j++){
                    int k=i*m+j;
                    if ((s&(1<<k))==0){ //해당 칸이 가로일 경우
                        current=current*10+a[i][j];
                    }
                    else{ //해당 칸이 세로일 경우: current를 0으로
                        sum+=current;
                        current=0;
                    }
                }
                sum+=current;
            }

위 링크는 가로로 묶은 숫자들을 다 더하는 경우이다. 

i 번째 열과 j 번째 행에 대해서 이중 for문을 설계하였고, 가로의 경우 하나의 열에 대해서 쭉 이어지는 식으로 구해야 하기 때문에 열을 나타내는 i가 바깥 for문이 되는 것이다. 

 

해당 칸이 가로일 경우 current 숫자 뒤에 해당 칸에 있는 숫자를 더하고, 그렇지 않을 경우 세로를 나타내는 것이기 때문에 sum에다가 지금까지의 수를 더해준 뒤 current는 초기화해준다.

 


세로의 경우도 마찬가지로 하되, 열과 행의 순서만 바꾸면 된다.

 //세로
            for(int j=0;j<m; j++ ){
                int current=0;
                for(int i=0; i<n; i++){
                    int k=i*m+j;
                    if ((s&(1<<k))!=0){
                        current=current*10+a[i][j];
                    }
                    else{
                        sum+=current;
                        current=0;
                    }
                }
                sum+=current;
            }

위의 내용을 종합해보았을 때 전체 코드는

import java.util.*;

public class Main{
    public static void main(String[] args){
        Scanner sc= new Scanner (System.in);
        int n=sc.nextInt();
        int m=sc.nextInt();
        int [][]a=new int [n][m];
        for(int i=0; i<n; i++){
            String s=sc.next();
            for(int j=0; j<m; j++){
                a[i][j]=s.charAt(j)-'0';
            }
        }
        int ans=0;
        //가로: 0, 세로: 1
        for(int s=0; s<(1<<(n*m)); s++){
            int sum=0;
            //가로 찾기
            for(int i=0; i<n; i++){
                int current=0;
                for(int j=0; j<m; j++){
                    int k=i*m+j;
                    if ((s&(1<<k))==0){
                        current=current*10+a[i][j];
                    }
                    else{
                        sum+=current;
                        current=0;
                    }
                }
                sum+=current;
            }
            //세로
            for(int j=0;j<m; j++ ){
                int current=0;
                for(int i=0; i<n; i++){
                    int k=i*m+j;
                    if ((s&(1<<k))!=0){
                        current=current*10+a[i][j];
                    }
                    else{
                        sum+=current;
                        current=0;
                    }
                }
                sum+=current;
            }
            ans=Math.max(sum, ans);
        }
        System.out.println(ans);
    }
}

이렇게 작성할 수 있다. 


2월 15, 2024

[백준] 1285번 동전 뒤집기 문제 쉽게 푸는방법?

 www.acmicpc.net/problem/1285

문제는 위의 링크를 클릭하면 볼 수 있다. 



이 문제는 비트마스크를 활용해서 풀 수 있다. 

우선 동전을 행과 열에 대해서 뒤집을 수 있기 때문에 행을 뒤집을 수 있는 경우, 즉 2의 N가지를 비트마스크를 통해서 표현해보는 것이다. (각 행에 대해서 뒤집는다/안뒤집는다 두 가지 choice가 있기 때문에) 그러면 비트마스크로 나타내면 0부터 n-1자리 수까지 즉 (1<<n)-1 까지 숫자가 있는것이고 k 자리 수가 1이라는 것은 k 번째 행을 뒤집는 것을 표현한 것이라고 할 수 있다. 

 

그래서 0부터 (1<<n)-1까지 모든 경우에 대해서 for문을 돌리고, 또 이중 for문으로 열에 대해서 뒤집을지 뒤집지 않을지를 결정해준다. 하지만 열의 경우에는 T의 최소개수를 고르는 것이기 때문에 현재 T와 H 중 최소값을 sum에다가 더해주면 된다. 이유는 T의 최소값을 구하는 것인데 만약 현재 T가 적게 있다면 그 숫자만큼 더해주면 되는 것이고, 만약 H가 더 적게 있다면 한번 뒤집었다고 생각하고 H의 개수를 단순히 더해주면 된다.

 

그런뒤에 비트마스크의 모든 경우마다 답을 구해주고 그것이 최소가 되는 경우를 찾아주면 된다.



이를 코드화한 것은 아래와 같다.

import java.util.*;

public class Main{
    static char change(char x){
        if (x=='H'){
            return 'T';
        }
        else {
            return 'H';
        }
    } 
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        char a[][]=new char[n][n];
        for(int i=0; i<n; i++){
            String s=sc.next();
            for(int j=0; j<n; j++){
                a[i][j]=s.charAt(j);
            }
        }
        int ans=n*n; //최소값을 구해야 하므로 최대부터 시작
        for(int bit=0; bit<(1<<n); bit++){
            int sum=0;
            for(int i=0; i<n; i++){//모든 세로에 대해서 구함
                int tail=0;
                for(int j=0; j<n; j++){//모든 가로
                    char cur=a[j][i];
                    if ((bit&(1<<j))!=0){//행을 뒤집어준다는 뜻
                        cur=change(cur);
                    }
                    if (cur=='T'){tail++;}
                }
                sum+=Math.min(tail, n-tail);
            }
            if (sum<ans){
                ans=sum;
            }
        }
        System.out.println(ans);
    }
}

 

즉 이 문제는 한 행에 대해서 어떻게 change할지를 비트마스크로 결정해놓고 그 다음에 각 열에 대해서 T와 H 중에 적은 값을 더해주면 쉽게 풀 수 있는 문제이다.