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);
    }
}

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


4월 17, 2024

[Linux/Ubuntu] 파일 오픈 시 Couldn't get a file descriptor referring to the console 에러 해결

1. Couldn't get a file descriptor referring to the console 에러 


html 파일을 ubuntu에서 실행시키기 위해 

open index.html

이라고 하면 

couldn't get a file descriptor referring to the console 이라는 에러가 뜰 때가 있다.


2. xdg-open 명령어로 해결하기

 

이를 해결하기 위해서는 

open 대신 xdg-open 명령어를 사용하면 된다.

xdg-open 명령어는 우분투에서 파일과 연관된 프로그램을 실행시켜주는 명령어이다. 따라서 html 파일 뿐 아니라 pdf, mp3, mkv와 같은 다양한 확장자의 프로그램을 실행시켜준다.

xdg-open index.html



html 파일을 xdg-open으로 실행하면, ubuntu에서 html 화면이 위와 같이 뜬다. 

실제 html과 마찬가지로 inspect 기능도 되고, console에 찍히는 것도 확인할 수 있어 편리하다.


4월 17, 2024

객체 지향 설계 원칙 (SOLID) 한방에 정리! 개발자라면 필수로 알아야 할 개념!

객체 지향 설계 원칙 (SOLID)란?

기술면접의 단골질문이자 시험에도 자주 나오는 SOLID 객체 지향 설계 원칙에 대해 알아보도록 하겠다. SOLID에 대해서는 개념적으로 숙지하고 있는 것도 좋지만 객체 지향 프로그래밍을 직접 할 때도 이를 참고하면 코드의 퀄리티를 높일 수 있기 때문에 꼭 알아두는 것을 추천한다. 

 

SOLID 원칙은 객체 지향 설계 원칙이라고도 불리며 객체 지향 프로그래밍 설계 시 필요한 다섯 가지의 원칙 (Single Responsibility Principle, Open Close Principle, Liskov Substitution Principle, Interface Segregation Principle, Dependency Inversion Principle)의 앞 글자를 따 붙여진 이름이다. 다섯 개 모두 다 중요한 의미를 가지고 있기 때문에 각각에 대해 자세히 알아보자.


1. Single Responsibility Principle 단일 책임의 원칙


비교적 간단한 원칙이다. 말 그대로 하나의 클래스는 하나의 책임만을 수행하게 하자는 뜻이다.

 

객체 지향이 절차 지향과 다른 점은 class를 가지고 객체를 생성할 수 있다는 뜻인데, 가끔 객체 지향 언어로 코드를 짜는 사람 중에서는 이러한 객체 지향의 장점을 적극 활용하지 않고 하나의 클래스에 모든 기능을 넣는 사람이 있다. 물론 메소드를 구분하여 기능을 분류할 수 있지만 엄연히 다른 일을 수행하는 객체의 경우 class를 구분하는 것이 더 맞는 방식이다. 만약 class를 책임 별로 구분하지 않는다면 하나의 class가 수행하는 일이 거대해져 객체 지향 프로그래밍의 의미를 잃게 된다.

 

따라서 단일 책임의 원칙 (Single Responsiblity Principle) 에서는 클래스가 제공하는 모든 서비스는 하나의 책임을 수행하는 데 집중되어 있어야 한다고 말하고 있다. 1번 원칙인 만큼 단일 책임의 원칙은 나머지 2~5번 원칙의 기초가 되는 원칙이다. 


2. Open Close Principle 개방 폐쇄 원칙


컴포넌트, 클래스, 모듈, 함수와 같은 소프트웨어 구성요소는 확장에는 열려있고, 변경에는 닫혀있어야 한다는 원칙이다.

 

Java 언어를 배운 사람들은 쉽게 이해할 텐데, 상속을 받아서 하위 클래스에서 overriding 등을 하는 것은 가능하지만, 외부 class에서 접근해서 특정 field를 바꾸는 것을 제한하기 위해서 접근 제한자(private, protected 등)를 사용한다. 즉 해당 class뿐 아니라 이외 소프트웨어의 구성요소를 확장하여 사용할 때는 open 되어 있는 반면, 해당 구성요소를 외부에서 임의로 변경하려는 시도는 제한하여 그 경우에는 closed 되어 있어야 한다는 원칙이다.

 

개방 폐쇄 원칙은 협업 개발 시 매우 중요하게 생각해보아야 할 원리로, 내가 개발한 구성요소가 다른 사람에게 적절한 때 개방되고 폐쇄되는지에 대해 신중히 고민해보아야 한다.

 


3. Liskov Substitution Principle 리스코프 치환의 원칙


상속받은 하위 클래스는 어디서나 상위 클래스로 교체할 수 있어야 한다는 원칙이다.

 

리스코프 치환의 원칙은 예시로 이해하는 것이 가장 빠른 것 같다. 다음은 리스코프 치환의 원칙에 위배되는 예시이다. 만약 '도형'이라는 상위 class가 있다고 가정해보자. 그리고 '도형'이라는 class에 '꼭짓점의 개수는 ~개이다'라는 것을 나타내주는 method가 있다고 가정해보자. 그리고 '도형'이라는 상위 class를 상속받아 '사각형'이라는 하위클래스와 '원'이라는 하위클래스를 만들어주었다. 사각형이라는 하위 클래스는 꼭짓점을 4개 가지고 있으니 상위클래스인 도형으로 교체가 가능하다. 하지만 '원'이라는 하위클래스는 상위클래스인 도형이 가지고 있는 메서드를 충족시킬 수 없으므로 리스코프 치환의 원칙에 위배된다. 리스코프 치환의 원칙에 위배되지 않게끔 클래스를 설계하려면 '꼭짓점이 있는 도형'과 '꼭짓점이 없는 도형'의 상위클래스를 설계하고 꼭짓점이 있는 도형을 상속받아 삼각형, 사각형 등의 하위클래스를 만들고, 꼭짓점이 없는 도형을 상속받아 타원, 원 등의 하위클래스를 만들어야 할 것이다. 


4. Interface Segregation Principle 인터페이스 분리의 원칙


class와 달리 인터페이스는 다중 구현이 가능하다. Java interface를 생각해보면 implements라는 keyword 이후에 여러 interface가 오는 것을 종종 본 적이 있을 것이다. 물론 여러 interface를 구현해도 되지만, 인터페이스 분리의 원칙에서는 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다고 말하고 있다. 즉, 클라이언트가 사용하지 않는 인터페이스 때문에 영향을 받아서는 안 된다는 원칙이다.

 

가끔 만들어둔 모든 인터페이스를 모두 구현하여 필요한 것만 사용하는 경우가 있는데, 물론 편리하겠지만 사용하는 인터페이스만 구현하도록 노력해야 할 것이다.


5. Dependency Inversion Principle 의존성 역전의 원칙 (DIP) 


의존성 역전의 원칙은 의존성 관계를 맺을 때 변화하기 쉬운 것보다는 변화가 없는 것에 의존관계를 맺어야 한다는 원칙이다. 간단하게 DIP라고 부르기도 한다. 만약 어떠한 상점이 할인정책으로 fixed amount discount policy, 즉 모든 물건에 대해 정해진 양 ex) 2000원 할인을 실시했다고 치자. 

의존성 역전의 원칙이 잘 지켜지지 않는 경우

discount policy로 fixed amount discount policy를 채택해서 위와 같은 diagram 형식의 class 구조가 되었지만, 사실 이는 5번 의존성 역전의 원칙을 잘 준수하지 않은 예시이다. 왜냐하면 만약 이후 상점이 마음이 바뀌어 할인정책으로 정량할인정책이 아닌, 정률할인정책을 사용한다면 단순히 fixed amount class를 바꾸어야 할 뿐만 아니라 store의 다른 영향을 받는 부분까지 모두 다 변경해야 하기 때문이다. 현재 store class는 변화가 있을 수 있는 fixed amount policy라는 class에 의존관계를 맺고 있기 때문에 의존성 역전의 원칙을 만족시키지 않는 것이다. 

 

의존성 역전의 원칙을 잘 만족시키기 위해서는 변화가 없는 interface나 상위 클래스와 의존관계를 맺어야 한다. 위의 예시를 수정하면 아래와 같은 다이어그램이 될 것이다. 



의존성 역전의 원칙이 잘 지켜지는 경우 

즉 store는 변화가 잘 없는 discount policies라는 interface와 의존관계를 맺고 있으며, 이럴 경우 상점이 택하고 있는 할인정책이 변하더라도 store 자체 class는 큰 변경 없이 사용이 가능하다. 만약 두 가지 할인 정책 이외에 새로운 할인정책을 변경하고 싶다면 discount policies 를 구현하는 새로운 하위 클래스를 만들어 사용하면 되기 때문에 문제가 없다. 오히려 개발자의 입장에서는 변경이 용이해진 것이다. 

 

의존성 역전의 원칙은 5가지 원칙 중 가장 이해하기가 까다로운 원칙이었을 것이다. 중요하지 않아 보일 수 있지만 이러한 의존성 역전의 원칙은 자바, 특히 이후 Spring에서 매우 중요하게 강조되는 개념이므로 잘 알아두면 유용할 것이다.


개발자라면 필수로 알아야 할 SOLID 원칙에 대해 알아보았다. 개념이 이해되었다고 넘어가지 말고 실제 코드 작성을 할 때 유념하여 의식적으로 이 원칙들을 잘 적용하고 있는지 살펴보면 좋을 것 같다. 


4월 17, 2024

C 언어 구조체 쉽게 이해하기, struct 올바른 사용법

1. C 언어 구조체란

C 언어에서 구조체란 영어로 struct, 즉 정형화된 structure (구조) 라고 생각하면 편하다. 

정해진 멤버들이 있고, 해당 멤버가 포함된 한마디로 해당 멤버들이 묶인 그룹이라고 생각하면 된다. 

 

가령 모든 사람에게는 공통적으로 '이름' 과 '나이' 라는 속성이 있다. 

그러면 우리는 '이름'과 '나이'라는 멤버를 가지는 '사람'이라는 구조체를 가지고 있는 것이다. 구조체는 이처럼 동일한 구조를 가진 객체라고 생각하면 된다. 하지만 자바의 객체와는 다른 점이 있으니 동일 규격으로 구성된 하나의 구조라고 생각하는 것이 더 정확할 것 같다. 


2. struct 키워드 

구조체를 사용할 때는 "struct" 라는 키워드를 사용한다.

 

struct 구조체이름{
 자료형 멤버이름1;
 자료형 멤버이름2;
 자료형 멤버이름3;
};

구조체의 기본적인 골격은 위와 같다. 구조체 { } 안에 원하는 멤버들을 자료형, 이름 순서대로 기입해주면 된다.

들어갈 수 있는 멤버의 수에는 제한이 없지만 멤버들이 많아질 수록 구조체도 방대해지는 것이라고 생각하면 된다.

 

위에서 말한 "사람"으로 구조체를 만들어보도록 하겠다. 사람 구조체의 멤버에는 나이와 이름이 있을 수 있고, 다른 attribute들이 추가적으로 있을 수 있으나, 공통적으로 존재하는 attribute여야 구조체의 활용도가 높아지므로 일단 나이와 이름이라는 멤버에만 국한하여 구조체를 만들어보겠다.

 

struct Person {   // 구조체 Person
   int age;              // 구조체 멤버 1: 나이
   char name[20];        // 구조체 멤버 2: 이름
};

이런식으로 구조체를 구성해보았다. 구조체를 선언한 뒤에는 반드시 세미콜론을 붙여주어야 한다는 점을 잊지 말자.

 

나이는 int형으로, 이름은 char[] 형으로 구성하여 멤버 두 개로 구성된 구조체 Person이 만들어졌다.

 

하지만 이렇게 선언만 했다는 것은 Person이라는 모형 (틀) 이 생겼다는 것이고 실제 사람 실체를 만들기 위해서는 구조체를 사용해 만들어주고 멤버들의 내용을 채워주어야 한다. 위에서 볼 수 있듯이 struct의 경우에는 대체로 main () 함수 밖에 정의하며, main 함수에서 변수를 선언하여 사용하는 형식이다. 

 

C언어 main 함수에서 객체를 생성해주고 멤버 정보를 채워보자.

int main()
{
    struct Person person1;     // 구조체에 해당되는 변수 선언
    
    person1.age = 50; //나이 할당
    // 이름에 해당하는 값 strcpy() function 통해 할당
    strcpy(person1.name, "김철수");

    
    //구조체에 정보가 잘 들어갔는지 프린트를 통해 확인
    printf("나이: %d\n", person1.age);        // 나이: 50
    printf("이름: %s\n", person1.name);       // 이름: 김철수
    return 0;
}

멤버 정보를 할당 할때 우리는 선언해준 구조체 변수에 . 을 붙이고 그 안에 member 이름을 붙여 접근을 하게 된다. 

즉 사람의 이름에 접근할때는 person1.name, 나이 값을 알고 싶다면 person1.age로 해서 접근을 하는 것이다. 

 

int 형의 자료형 같은 경우에는 

person1.age = 50

 이런 식으로 선언을 해줄 수 있지만,

 

char [] 형의 경우에는 할당 연산자를 사용할 수 없기 때문에 strcpy() 를 사용한 것이다. 

strcpy(person1.name, "김철수");

이런식으로 적어주면 되고, strcpy() import를 위해서는 

#include <string.h>

를 맨 위에 적어주면 된다.

 


3. 최종 예시 코드

이렇게 완성된 최종 코드는 아래와 같다.

#include <stdio.h>
#include <string.h>    // strcpy function import를 위해 
#define _CRT_SECURE_NO_WARNINGS   // strcpy 보안 경고 방지

struct Person {   // 구조체 Person
   int age;              // 구조체 멤버 1: 나이
   char name[20];        // 구조체 멤버 2: 이름
};

int main()
{
    struct Person person1;     // 구조체에 해당되는 변수 선언
    
    person1.age = 50; //나이 할당
    // 이름에 해당하는 값 strcpy() function 통해 할당
    strcpy(person1.name, "김철수");

    
    //구조체에 정보가 잘 들어갔는지 프린트를 통해 확인
    printf("나이: %d\n", person1.age);        // 나이: 50
    printf("이름: %s\n", person1.name);       // 이름: 김철수
    return 0;
}

 

이런식으로 C 언어에서 struct 구조체를 사용하는 방법을 알아보았다. 

간단하지만 모르면 C 언어 코드를 이해하기 어려운 경우가 많으니 잘 알아두자. 


4월 17, 2024

[웹개발] 개발자 도구 사용하는 법

웹에서 개발자 도구 사용하는 법

웹개발을 하는 개발자들에게 특히나 유용한 "개발자 도구" 사용하는 방법을 소개하려고 한다. 개발자 도구는 코드를 변경하지 않고 무언가를 테스트하거나 css 속성을 파악하거나, 웹개발 디버깅을 하는데 특히나 유용하다. 

 

일단 개발자 도구를 키기 위해서는 F12 혹은 Control + Shift + I를 누르면 된다. 혹은 웹 상에 우클릭을 한 뒤, "검사" 버튼을 눌러도 동일하게 개발자 도구가 실행된다. 

 

사실 개발자 도구만 키면 어떻게 사용하는지 절반은 배우게 된 것인데, 키면 아래와 같이 확인할 수 있는 html과 css 부분이 나온다. 

 

가장 쉽게 접할 수 있는 네이버 웹을 통해 예시를 살펴보자.

 


네이버 개발자 도구

 

위와 같이 키면 여러 html과 css를 확인할 수 있다. 

 

개발자 도구에 보면 elements, console, sources, network 이런식으로 구분이 되어있는데 오늘은 이 중 가장 유용하게 활용할 수 있는 elements와 console 탭에 대해 알아보도록 하겠다. 




1. Elements 탭

 

먼저 Elements 탭의 경우 



위 1 번이라고 써있는 부분을 클릭 후 알고자 하는 부분을 클릭하게 되면 오른쪽과 같이 html 전체 코드 중 해당 클릭된 부분의 html 부분을 알 수 있다. 클릭하면 해당 부분의 색깔도 #03C75A (구글에 hex color code라고 검색하면 더 자세히 알 수 있을 것이다) 

 

여기서 css 같은 경우에는 코드에서 직접 변경하지 않고 해당 html, css 부분에서 변경하면 변경한 내용으로 즉시 웹상에서 확인이 가능하다. css 의 경우에 이렇게 고치면 원하는 방식대로 될 것이라는 확신이 없는 경우에 위 웹상에서 변경하고 즉시 변경된 웹으로 확인이 가능하다는 장점이 있다. 


2. Console

 다음으로는 console tab에 들어가보자. 



 

console 탭은 특히 디버깅 하기에 좋다. 

 

화면 단 코드에 

console.log("버튼 클릭");

이런식으로 적어두면 해당 버튼을 클릭했을 시 해당 메세지가 콘솔 창에 나타나면서 어떤 이벤트를 타고 호출하는지 일련의 과정을 확인할 수 있다. 

 



console창 예시

위는 티스토리 작성 웹 화면이 개발자 도구 예시인데 위와 같이 console 창에 디버그 메세지가 남는 것을 확인할 수 있다.

 

또한 만약 화면단에서 스크립트 오류가 나서 더 이상 진행이 되지 않을 경우에는 콘솔창에 빨간 메세지로 원인을 알려주기 때문에 매우 유용하다.

 

화면개발이 완료되서 배포까지 된 정식 사이트의 경우에는 개발자도구를 사용할 일이 거의 없지만 로컬 환경에서 테스트를 해보고 있는 웹 개발자에게 특히나 유용한 기능이다. 

 

또한 정식으로 어느 코드를 타면서 어떤 액션이 취해지는지 디버그를 하기 위한 방법으로 

 

코드단에서 

debugger;

를 삽입을 해 놓으면 해당 코드부터 하나하나 액션이 취해지는 코드를 타고 디버그가 가능하니 매우 유용한 기능이다. 


4월 17, 2024

GET과 POST의 차이

GET과 POST의 차이

HTTP 메서드 중 유사한 기능을 담당하는 것 중 GET과 POST가 있다. 

비슷한 기능을 담당하고 있지만 보안이 더 요구될때는 post를 쓰는 것이 더 좋은 선택이다.

 

하나씩 개념을 알아보도록 하자.

 

1. GET

클라이언트에서 서버로 정보를 요청하기 위해 사용되는 메서드이다. 위에서 보안이 더 요구될 때 post를 써야 한다고 했는데 그 이유는 GET을 통해 요청을 보내면 URL 주소 끝에 매개변수로 포함되어 전송되기 때문이다. 이러한 것을 query string이라고 부르고 대략적인 형태는 url 뒤에 ? 를 붙이고 변수명=값&변수명=값 ... 이런식으로 이어붙여지게 된다. 

 

게시판 글의 경우 GET을 쓰는 것이 가능하다. 예를 들어 아래의 URL를 살펴보자.

https://programmingstory.com/bbs/board.php?bo_table=posting&wr_id=6&page=4 


/bbs/board.php 뒤 url에 

? 가 붙고 그 다음에 bo_table (변수에 해당) = posting (값에 해당) & wr_id(변수에 해당)=6(값에 해당) &page(변수에 해당)=4(값에 해당) 

이런 형식을 보인다는 것을 알 수 있다. 

 

따라서 GET은 이렇게 URL에 다 정보가 노출되기 때문에, 중요한 정보를 다룰때 사용하면 안된다는 것을 꼭 기억해야 한다. 또한 GET 요청은 북마크될 수 있으며 브라우저 히스토리에 남는다. 또한 뒤의 POST에서 설명이 나오겠지만 업데이트도 할 수 있는 POST와는 달리 GET은 말 그대로 데이터를 요청할 때만 사용이 된다. 

GET은 URL의 매개변수에 요청하는 데이터를 담아서 보내기 때문에 HTTP 메세지에 body가 존재하지 않는다. 이 부분도 뒤에 나오게 되는 POST와의 차이점이 될 수 있다. (POST는 body에 데이터를 담아 보낸다)


2. POST

POST는 GET과 달리, 클라이언트에서 서버로 리소스를 생성하거나 업데이트를 위한 데이터를 보낼 때 사용되는 HTTP 메서드이다.

게시판의 글을 단순히 볼 때 GET이 사용되었다면, 게시물을 올라거나, 업데이트 할 때 POST가 사용된다. 웹개발을 해본 사람들은 모두 익숙하겠지만 html form을 사용하여 POST 요청을 하는 것이 일반적이다. 

 

POST는 GET과 달리 브라우저 히스토리가 남지 않으며, 북마크되지도 않는다. 또한 GET에서는 HTTP 메세지에 body가 존재하지 않았다면 POST에서는 body에 데이터를 담아서 전송한다. POST 요청을 할 때는 데이터 길이에 제한이 없어서 큰 용량의 데이터를 다룰때 주로 사용한다. 

 

보안 부분도 추가적인 작업이 필요하지만, 우선 URL에 노출되는 GET보다 안전한 방식이기 때문에 보안이 중요한 데이터의 경우 POST를 사용해야 한다. 


4월 17, 2024

[네트워크] 네트워크 구성 요소

네트워크의 구성요소

네트워크의 구성요소를 나누는 데는 여러 방법이 있지만 오늘은 크게 network edge, access network, network core라는 세 가지 용어에 대해서 알아보도록 하겠다.

 

1. network edge:

network edge는 각각의 사용자가 직접 사용하는 기기를 포함하는 용어이다. 인터넷과 연결되는 end system이라는 뜻으로 network의 edge에 있다는 말이다. network edge는 서버, 데스크탑, 모바일을 모두 포괄하는 단어이다. 이러한 end system은 host라고 불리기도 하며 host는 client 와 server 두 가지로 분류될 수 있다. client는 서비스를 사용하는 사용자의 입장에서의 장비이고 서버는 서비스를 제공하는 쪽을 일컫는다. 대부분 서버는 데이터 센터에 위치해있을 가능성이 높다. cellular network의 장비 또한 network edge에 해당 될 수 있고, IOT와 같은 사물인터넷, 심지어 신호등까지 모두 end system에 포함된다고 한다. 

 

2. network core:

network core는 end user들은 접속되어 있지 않고 router들끼리 상호연결되어 있는 형태라고 할 수 있다. 한마디로 interconnected routers라고 할 수 있겠다.

 

3. access network:

end system을 상호연결시켜주는 네트워크를 access network라고 할 수 있다. 다시 말해 edge system을 다른 edge router에 연결해주는 네트워크라고 볼 수 있다. 그런데 이들이 상호연결되려고 하면 반드시 매체가 필요하다. 그래서 access network를 위해서는 각종 유선/무선 통신 link가 필요하다고 볼 수 있다. 

edge system을 edge router에 연결시키기 위해서는 residential access network (거주), institutional access network (기관), mobile access network 등 다양한 방법을 사용할 수 있다. access network에서는 매체를 사용해서 접근을 하는 것이기 때문에 매체가 어느 정도의 접근속도를 낼 수 있는지 (bandwidth (bits per second) ), 이 매체가 여러 유저 사이에 공유된 것인지 등 다양한 이슈가 대두되고 있다. 

 

access network 중에서 가장 기본적인 기법은 DSL (digital subscriber line) 을 살펴보겠다. 

  • DSL (Digital Subscriber Line)

출처: Computer Networking _ A Top Down Approach, 7th, converted

이는 집집마다 전화선이 이미 깔려있기 때문에 이 전화선을 사용해서 인터넷의 데이터를 실어나르는 데 사용하겠다라는 것이다. 그래서 위 그림에 보이는 DSL modem은 PC의 디지털 데이터를 아날로그 데이터로 바꿔주는 역할을 한다. 그래서 전화선을 통해 전화선의 유선 신호뿐만 아니라 컴퓨터 신호도 타고 가는 것이다. 그 말은 즉, voice와 data가 한꺼번에 전송된다는 것이다. 그러면 이 둘이 뒤섞일 수 있다는 문제점이 있다. 하지만 이는 서로 다른 주파수로 보내져서 한꺼번에 보내도 수신측에서 구분해낼 수 있다. 이렇게 보내면 DSLAM (DSL access multiplexer)에서 주파수를 분리해서 data면 ISP router로 보내고 voice 관련된 것이면 telephone network로 내보내게 된다. 즉 다시 말해 DSL phone 상에 data와 voice를 함께 실어서 central office 내의 multiplexer가 구분하게 되는 것이다.

 

하지만 이렇게 보내는 것이 꼭 한 방향으로만 이루어진다는 보장은 없다. 밖에서 인터넷/ 전화 데이터가 들어올 수도 있기 때문에 양방향적으로 통신이 된다. 우리는 central office -> 집 의 방향을 downstream이라고 이야기하고, 집 -> central office의 방향을 upstream이라고 이야기한다. 이렇게 양방향적으로 통신이 되기 때문에 downstream과 upstream이 하나의 line 에서 다 섞이게 된다. 그럼에도 불구하고 이러한 장비들이 다 분리해서 데이터와 voice를 원하는 쪽으로 배분할 수 있다. 

 

대부분 upstream에 비해서 downstream이 속도가 훨씬 높다. downstream은 이론적으로 24 Mbps 이하, upstream은 이론적으로 2.5 Mbps 이하로 확연히 차이가 난다. 현실적으로 따지면 downstream은 10 Mbps 정도, upstream은 1 Mbps 정도의 수치를 띈다고 한다. 

 

하지만 home network라고 해서 DSL만 쓰는 것은 아니다. 이제 다음으로 소개할 것은 cable network이다.

  • Cable network

출처 : Computer Networking _ A Top Down Approach, 7th

Cable network는 DSL과 다르게 전화기 대신 텔레비전이 연결되어 있는 것을 알 수 있고 컴퓨터에는 cable modem이 연결되어 있다. 주로 동축 케이블 (coaxial cable)을 사용한다. 그리고 cable network에서 한 가지 특징적인 것은 한 집과 cable headend라고 말하는 사업자쪽 연결이 혼자만 이루어진 것이 아니라 한 라인 상의 여러 집의 케이블들이 동시에 연결된 형태이다. 즉 한 집뿐만이 아니라 여러 집이 공유한다는 것이다. cable network도 DSL과 마찬가지로 upstream, downstream, 다양한 사용자들의 데이터가 한 라인에 모두 섞이게 된다. 그렇다면 이를 구분할 수 있는 방법이 필요한데 이 방법은 다른 주파수로 실어서 데이터를 나르는 것이다. 전체 주파수 밴드가 있다면 그것을 세부 조각으로 쪼개서 각각 집의 데이터를 다른 주파수로 실어서 나르는 것이다. 그래서 우리는 이 방식을 "frequency division multiplexing"이라고 부른다. 

 

케이블 네트워크 중에서 우리에게 가장 잘 알려져 있는 것이 HFC (hybrid fiber coaxial)이다. HFC는 이름이 의미하는 것처럼 fiber와 coaxial이 hybrid로 섞여 있기 때문에 hyrbid fiber coaxial이라고 부른다. 

 

cable network도 DSL과 마찬가지로 downstream이 upstream보다 데이터 양이 많기 때문에 downstream 파트에 데이터 속도가 더 빠르다. downstream의 데이터 양에 비해 upstream의 데이터 양이 상대적으로 적고 균형이 맞지 않는다고 해서 우리는 이를 asymmetric하다고 한다. 


4월 07, 2024

Wireless access network

1. Wireless Access Network

end system을 router로 연결시키는 access network를 wireless access network라고 한다. 말 그대로 '무선' 에 초점을 맞춘 개념이다. 

 

wireless access network에는 크게 두 가지 종류가 있는데 오늘은 이 두 종류를 살펴보겠다.

 

1) wireless LANs: 

LAN은 Local Area Network의 약자로 local area 직경 1km 이내에 device가 연결될 때 그것을 LAN이라고 한다. 이는 하나의 빌딩, 오피스, 집을 무선으로 연결시키는 것이고 이 때 사용하는 것이 802.11에서 나오는 각종 wifi protocol인 것이다. 조금 더 높은 속도, 안정적인 전송이 가능하게끔 하기 위해 802.11 b/g/n 등 다양한 버전이 나왔고 최근에는 802.11 ay가 나와 20 Gbps 무선촉감인터넷을 가능하게끔 하고 있다. 

 

2) wide-area wireless access:

wide area network를 의미하고 LAN보다 더 넓은 개념이다. 이것의 대표적인 예시가 cellular network이다. cellular 라는 단어를 쓰는 이유는 여러 개의 cell을 써서 넓은 영역을 커버한다고 해서 붙여진 이름이다. cellular network는 수십km, 수백km 까지 커버가 가능하다. 이름이 의미하는 바처럼 cell을 계속해서 확장하면 되기 때문이다. cell 사이에 통신이 가능해야 하기 때문에 cellular network가 여러 네트워크 중에서 어렵고 복잡한 축에 속한다. 3G, 4G, LTE 등 다양한 것이 나왔는데 이는 wireless LAN보다는 속도가 느리지만 6G로 가면 유선 광케이블정도까지 속도를 높일 수 있다고 한다. 


4월 07, 2024

데이터 전송/ 전송 속도/ bandwidth

1. packet 통신

통신 어플리케이션 프로그램은 프로그램 혼자 작동하는 것이 아니라 컴퓨터 내에 데이터를 주고 받으면서 데이터를 기반으로 한 action을 취하고 이를 통해 사용자가 원하는 서비스를 제공해주는 것이다. 이렇게 데이터를 주고받을 때 어떤 경우에는 1,2byte처럼 작은 양의 데이터를 요구할 수도 있지만 어떤 경우에는 많은 양의 데이터를 요구할 수도 있다. 이렇듯, data size가 다 다르기 때문에 다양한 사이즈의 데이터를 일정한 사이즈로 잘라서 이를 개별적으로 포장해서 보내는 과정이 필요하다. 우리는 이 기본단위를 packet이라고 부른다. 


2. packet transmission delay

L bit (주로 1200 byte)를 보낸다고 하면 우리는 이것이 몇번째 chunk이고 크기가 어떻게 되고 이러한 추가 정보를 앞에 적어서 함께 보낸다. 이를 우리는 header를 붙인다고 말한다. 만약 우리가 매체의 초당 속도가 R bit라고 한다면 이 패킷을 내보내는 데 걸리는 시간은 자연스럽게 L/R이 된다. 

 

그러면 우리는 이를 packet transmission delay 라고 부른다. L bit packet을 보내는 데 걸리는 시간, 다시 말해 packet의 첫 비트가 링크를 타기 시작하는 순간부터 마지막 bit 가 링크를 타기까지 걸리는 시간을 의미하는 것이다. 

 

3. 전송속도/ bandwidth

생각해보면 R (1초에 보낼 수 있는 bit) 가 클수록 시간이 적게 걸릴 것이다. 따라서 우리는 R을 transmission rate/ 링크의 최대 전송 속도/ link bandwidth/ capacity 등 다양한 용어로 부른다. 


4월 07, 2024

[유니온 파인드] union find 경로 압축

1. union find 경로 압축

Union Find에서 Find 를 할 때 루트가 가장 최 상단에 있다면 시간복잡도가 O(N) 이 나올수가 있기 때문에 굉장히 느리다. 

 

이런 문제점을 해결하기 위해서 우리는 경로 압축이라는 방법을 사용할 수 있다. 

 

부모 루트를 계속 연결시키는 것이 아니라 최상단의 노드를 부모로 정하는 것이다. 이렇게 구현하게 되면 트리의 모양은 바뀌지만 우리가 구현하려고 하는 알고리즘에는 전혀 지장을 주지 않는다. 

 

2. Find 함수 수정 


그래서 우리는 Find 함수를 아래와 같이 조금 수정해주려고 한다.

 

int Find(int x) {
 if (x==parent[x]){
  return x; //루트일 경우 루트 노드 return
 }
 else{
   int y=Find (parent[x]);
   parent[x]=y; // 부모 노드를 최상단 루트로 바꾸어줌
   return y;
 }

}

위 사이트에 올라와있는 코드를 그대로 사용했는데 여기에 parent[x]=y 라는 코드가 추가된 것이다. 

 

이렇게 하면 우리는 O(N)이던 연산의 시간을 O(α(N)) 으로 확실히 줄일 수 있다.


4월 07, 2024

보안 관련 용어 알아보기 malware/virus/worm/DDos/sniffing/spoofing

1. 네트워크 통신 보안 용어

네트워크 통신에 있어서 보안은 굉장히 중요한 주제이기 때문에 보안을 철저히 신경써야 한다는 것은 누구나 아는 사실일 것이다. 요즘에는 공격에도 다양한 종류가 있어서 이에 대한 다양한 용어에 대해 알아볼 필요가 있다.


1) malware:

malware는 나쁜 공격을 총체적으로 이르는 말이다. 기기에 침투해서 기기를 감염시키고 하는 행위들을 malware라고 한다. mal은 접두사로 나쁘다는 뜻인데 그렇기 때문에 나쁜 것들을 지칭해서 이르는 말이다. 아래 설명하는 공격행위를 모두 포괄하는 넓은 단어라고 할 수 있다.

 


2) virus:

우리가 흔히 알고 있는 바이러스는 자가 복제 기능을 가진 malware 중에 대표적인 예시이다. 바이러스의 대표적인 특징은 user interaction이 있어야 실행된다는 것이다. 다시 말해 대상이 되는 device, program에 일단 침투가 되면 그 프로그램이 실행되어야 그것이 비로소 활성화되는 것이다. User interaction이 있어야 malfunction이 되는 것이다. 그 중에 하나의 예시로 이메일로 바이러스가 침투해있는 것을 들 수 있다. 그 경우 사용자가 이메일을 열어봤을 경우에 바이러스가 퍼지게 되는 것이다. 그런데 자가복제기능을 가지고 있기 때문에 첨부파일에 있던 악성코드가 활성화되어서 이메일 시스템에 온갖 receiver들에 대해 전부 자기를 복제해서 똑 같은 메일을 뿌리게 된다. 바이러스의 자기복제기능도 중요하지만 대상이 되는 파일이 실행될 때만 문제가 되는 것을 기억하는 것이 더 중요하다.


3) worm:

worm도 바이러스와 유사하지만 가장 큰 차이로는 user experience가 없어도 실행될 수 있다는 것이다. 네트워크에 연결되어있고 취약한 어플리케이션이 돌고 있으면 거기에 침투되어 스스로 실행될 수 있다.


4) spyware:

spyware은 이름 그대로 spy 적인 성격을 가지고 있다. 사용자가 모르게 들어와서 그 컴퓨터에 있는 주요정보를 수집해서 spyware를 침투시킨 해커에게 보고하는 성격이다. spy처럼 몰래 침투해서 주요 정보 keystroke 계속 분석하거나 비밀번호를 입력할 때 몰래 정보를 수집하는 식으로 이루어진다.


5) botnet:

인터넷에 연결되어 있으면서 해를 입은 여러 컴퓨터들의 집합을 지칭한다. 아래 DDos 공격에서 사용되는 용어이다. 즉 botnet은 하나의 컴퓨터가 아니라 여러 컴퓨터들의 집합이며, 이들은 모두 사이버범죄자가 악성 소프트웨어를 이용해 빼앗은 좀비 컴퓨터로 구성되어 있다. 쉽게 말해서 공격자가 주변의 많은 컴퓨터에게 악성코드를 심고 그것에 의해 감염된 컴퓨터의 집합체라고 이해하면 될 것 같다.


6) DDos: Distrubited denial-of-service

타겟을 하나 정하고 디도스 공격을 한다고 한다. 디도스 공격을 하기 전에 타겟 주변에 있는 많은 컴퓨터(호스트들)를 감염시켜놓는다. 이것들이 위에서 언급했던 botnet을 형성한다고 얘기하는 것이다. 그러면 감염된 Botnet의 각 컴퓨터들이 타겟으로 쓸데없는 패킷을 보낸다. 그러면 타겟 컴퓨터입장에서는 너무 많은 트래픽이 한꺼번에 들어오기 때문에 정상적인 서비스를 거부하고 못하는 것이다. 그래서 이것을 정상적인 서비스의 거부라고 해서, Denial of Service라고 하는 것이다. 이런 공격을 하는 것을 것을 여러 botnet을 통해서 하는 것이기 때문에 Distributed Dos 공격이라고 부른다.


7) Sniffing:

어떤 컴퓨터가 destination computer로 데이터를 보내게 되면 주변의 악성 공격자가 지나가고 있는 트래픽을 몰래 훔쳐보는 행위를 일컫는다. 이렇게 몰래 트래픽을 훔쳐보면서 민감한 정보들 (개인정보, 보안 비밀번호 등등)을 얻을 수 있는 것이다. 공유매체 사용하는 Ethernet이나 무선처럼 공중에 뿌리는 것( broadcast 매체)의 경우 정보를 공중에다가 뿌릴 수밖에 없는데 그러면 지나가는 모든 packet들은 몰래 들을 수 있다. 이렇기 때문에 sniffing이라는 행위가 발생하는 것이고 따라서 무선에서는 유선보다 보안이 어려운 것이 사실이다. 이러한 sniffing은 passive한 성격을 띄고 있기 때문에 더 감지하기도 어렵다. 그래서 이렇게 sniffing이 발생할 행위를 염두해 두고 암호화하여서 정보를 보내는 것이 필요할 때가 있다.


8) Spoofing:

예를 들어 컴퓨터 A와 컴퓨터 B와에는 신뢰관계가 구축되어 있어서 컴퓨터 B가 보낸 것이라면 컴퓨터 A는 검사를 하지 않는 상황이 있다고 가정해보자. 그런데 만약 악의적인 사용자 컴퓨터 C가 파일을 보내면서 자신이 아니라 source B가 보내는 것처럼 가장하면서 source B의 주소를 보내기도 한다. 이러면 컴퓨터 A는 컴퓨터 B로부터 온 줄 알고 추가 검사를 하지 않은 채로 파일을 받게 된다. 이런 식으로 spoofing이란 악의적인 사용자가 다른 사용자인척 가장하고 무엇인가를 보내는 상황을 지칭한다. 그래서 spoofing을 방지하기 위해서는 end-point authentication, 즉 정말 이 메세지가 거기로부터 온 것인지에 대한 검사를 철저히 해주어야 한다. 


4월 07, 2024

[백준] 17088번 등차수열 변환 문제 풀어보기

1. 문제

www.acmicpc.net/problem/17088

문제는 위 링크에 들어가면 확인할 수 있다.


2. 풀이

이 문제는 등차수열의 성질을 활용하여 푸는 문제인데, 첫째항과 공차를 결정하여서 풀 수 있다.

 

가능한 첫째항은 a1, a1+1, a1-1 이렇게 세 가지가 가능하고, 마찬가지로 둘째항은 a2, a2+1, a2-1 이렇게 세 가지가 가능하다. 

 

그러면 총 3*3= 9가지의 경우의 수가 존재하고 첫째항과 공차가 결정되므로 모든 항에 대해서 가능한지 결정해보면 된다. 


3. 코드 

따라서 이를 코드로 구현한 것은 아래와 같다.

import java.util.*;

public class Main{
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] a = new int[n];
        for (int i=0; i<n; i++) {
            a[i] = sc.nextInt();
        }
        if (n == 1) {
            System.out.println(0);
            System.exit(0);
        }
        int ans=-1;
        for(int c1=-1;c1<=1; c1++ ){
            for(int c2=-1; c2<=1; c2++){
                int change=0;
                if (c1!=0) change++;
                if (c2!=0) change++;
                int a1=a[0]+c1;
                int d=a[1]+c2-a1;
                boolean ok=true;
                int cur=a1+d;
                for(int i=2; i<n; i++){
                    cur+=d;
                    if (a[i]==cur) continue;
                    else if (a[i]+1==cur || a[i]-1==cur){
                        change++;
                    }
                    else{
                        ok=false;
                        break;
                    }
                }
                if (ok) {
                    if (ans == -1 || ans > change) {
                        ans = change;
                    }
                }
            }
        }
         System.out.println(ans);
    }
}

 

여기서 change란 숫자를 변경하는 횟수를 뜻한다. 1 차이가 나면 1번 변경하면 가능하기 때문에 change를 1 증가시키는 것이다.


4월 07, 2024

[백준] 16943번 숫자 재배치 문제 풀어보기

1. 문제

www.acmicpc.net/problem/16943

문제는 위 링크에 들어가면 확인할 수 있다.


2. 풀이

위 풀이에서 매번 순열을 하는 코드는 자바로 아래와 같이 구현할 수 있다.

 static boolean next_permutation(char[] a) {
        int i = a.length-1;
        while (i > 0 && a[i-1] >= a[i]) {
            i -= 1;
        }

        if (i <= 0) {
            return false;
        }

        int j = a.length-1;
        while (a[j] <= a[i-1]) {
            j -= 1;
        }

        char temp = a[i-1];
        a[i-1] = a[j];
        a[j] = temp;

        j = a.length-1;
        while (i < j) {
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
            i += 1;
            j -= 1;
        }
        return true;
    }

자바에서 순열을 만드는 코드이다. 


또한 우리가 문자로 입력을 받았기 때문에 이를 다시 숫자로 돌리는 과정이 필요하다. 

C++의 경우에는 위의 풀이처럼 stoi를 써야 하지만 자바는 이를 또 구현해주는 과정이 필요하다. 

 

char 배열을 int로 바꿔주는 부분의 함수는 아래와 같다.

 

   static int toNum(char[] a) {
        int ans = 0;
        for (char ch : a) {
            ans = ans * 10 + (ch - '0');
        }
        return ans;
    }

따라서 매번 순열로 순서를 바꾸어주고 이를 다시 toNum 함수를 사용해서 숫자로 바꾸어주는 것이다. 그런 다음에 문제의 조건을 만족하는지 살펴보면 된다.

 

3. 코드 


전체 코드는 아래와 같다.

import java.util.*;
public class Main {
    static boolean next_permutation(char[] a) {
        int i = a.length-1;
        while (i > 0 && a[i-1] >= a[i]) {
            i -= 1;
        }

        if (i <= 0) {
            return false;
        }

        int j = a.length-1;
        while (a[j] <= a[i-1]) {
            j -= 1;
        }

        char temp = a[i-1];
        a[i-1] = a[j];
        a[j] = temp;

        j = a.length-1;
        while (i < j) {
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
            i += 1;
            j -= 1;
        }
        return true;
    }
    static int toNum(char[] a) {
        int ans = 0;
        for (char ch : a) {
            ans = ans * 10 + (ch - '0');
        }
        return ans;
    }
    public static void main(String args[]) {
        Scanner sc = new Scanner(System.in);
        char[] a = sc.next().toCharArray();
        int b = sc.nextInt();
        Arrays.sort(a);
        int ans = -1;
        do {
            int number = toNum(a);
            if (a[0] != '0' && number < b) {
                if (ans == -1 || ans < number) {
                    ans = number;
                }
            }
        } while (next_permutation(a));
        System.out.println(ans);
    }
}

4월 07, 2024

[백준] 17142번: 연구소 3문제 BFS로 풀기

1. 문제 

www.acmicpc.net/problem/17142

문제는 위 링크에 들어가서 볼 수 있다.


2. 풀이

이 문제는 먼저 바이러스를 놓을 수 있는 칸에서부터 시작해 바이러스를 m개 놓아야 한다. 그것을 우리는 재귀함수 move로 구현하겠다. 항상 그랬듯이, index와 count를 parameter로 받고 정해진 지점에 도달했을 때, 우리는 bfs를 돌리면서 검사를 하는 것이다. 

 

move의 함수는 아래와 같다.

    static ArrayList<Integer> xs = new ArrayList<>();
    static ArrayList<Integer> ys = new ArrayList<>();
 public static void move(int index,int count){
        if (index==xs.size()){
            if (count==m){
                bfs();
            }
        }else{
            int x = xs.get(index);
            int y = ys.get(index);
            a[x][y] = 3; //바이러스 놓기
            move(index+1, count+1);
            a[x][y] = 2; 
            move(index+1, count);
        }
    }

여기서 xs는 가능한 바이러스의 위치를 알려주는 ArrayList이다. 문제에서 입력받을 때 바이러스를 놓을 수 있는 위치의 행을 xs에, 열을 ys에 저장한다. 그러면 index의 위치가 xs의 size와 같다는 것은 전체 바이러스를 다 검사했다는 것이고, 그 중에서 count의 값이 m이라는 것은 문제의 조건과 일치한다는 뜻이므로 그 때 bfs 함수를 호출해 bfs 검사를 해주면 된다.


 static void bfs(){
        int d[][]=new int[n][n];
        Queue<Integer>q=new LinkedList<>();
         for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                d[i][j] = -1;
                if (a[i][j] == 3) {
                    q.add(i);q.add(j);
                    d[i][j] = 0;
                }
            }
        }
        while(!q.isEmpty()){
            int x=q.remove();
            int y=q.remove();
            for(int k=0; k<4; k++){
                int nx=x+dx[k];
                int ny=y+dy[k];
                if (0<=nx && nx<n && 0<=ny && ny<n){
                    if (a[nx][ny] != 1 && d[nx][ny] == -1) {
                        d[nx][ny] = d[x][y] + 1;
                        q.add(nx); q.add(ny);
                    }
                }
            }
        }
        int val = 0;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] == 0) { //이미 빈칸이었던 애들만 검사
                    if (d[i][j] == -1) return;
                    if (val < d[i][j]) val = d[i][j];
                }
            }
        }
        if (ans == -1 || ans > val) {
            ans = val;
        }
    }

위 코드는 bfs 함수만 적어본 것이다. 일반적인 BFS 코드와 유사하나 다른 점은 문제에서 빈칸이 바이러스에 감염되는 시간을 검사하라고 했으므로 원래 칸이 빈칸이었는지를 하나 더 검사해주어야 한다. 

 


3. 풀이

전체 JAVA 코드를 살펴보면 아래와 같다.

import java.util.*;

public class Main{
    static int[][] a;
    static int[][] d;
    static int[] dx = {0,0,1,-1};
    static int[] dy = {1,-1,0,0};
    static int n, m;
    static ArrayList<Integer> xs = new ArrayList<>();;
    static ArrayList<Integer> ys = new ArrayList<>();;
    static int ans = -1;
    static void bfs(){
        d=new int[n][n];
        Queue<Integer>q=new LinkedList<>();
         for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                d[i][j] = -1;
                if (a[i][j] == 3) {
                    q.add(i);q.add(j);
                    d[i][j] = 0;
                }
            }
        }
        while(!q.isEmpty()){
            int x=q.remove();
            int y=q.remove();
            for(int k=0; k<4; k++){
                int nx=x+dx[k];
                int ny=y+dy[k];
                if (0<=nx && nx<n && 0<=ny && ny<n){
                    if (a[nx][ny] != 1 && d[nx][ny] == -1) {
                        d[nx][ny] = d[x][y] + 1;
                        q.add(nx); q.add(ny);
                    }
                }
            }
        }
        int val = 0;
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                if (a[i][j] == 0) { //이미 빈칸이었던 애들만 검사
                    if (d[i][j] == -1) return; //불가능
                    if (val < d[i][j]) {val = d[i][j];}
                }
            }
        }
        if (ans == -1 || ans > val) {
            ans = val;
        }
    }
    public static void move(int index,int count){
        if (index==xs.size()){
            if (count==m){
                bfs();
            }
        }else{
            int x = xs.get(index);
            int y = ys.get(index);
            a[x][y] = 3; //바이러스 놓기
            move(index+1, count+1);
            a[x][y] = 2; 
            move(index+1, count);
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        a = new int[n][n];
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                a[i][j] = sc.nextInt();
                if (a[i][j] == 2) {
                    xs.add(i);
                    ys.add(j);
                }
            }
        }
        move(0,0);
        System.out.println(ans);
    }
}