질문

더보기
  1. 프로세스와 쓰레드의 차이점은?
  2. 메모리 영역 중 힙영역과 스택영역의 차의점은?
  3. 쓰레드에서 stack을 분리한 이유는?
  4. 멀티 프로세싱과 멀티 쓰레딩의 장단점 및 차이점은?
  5. 멀티 쓰레드 환경에서의 주의점
  6. CPU 스케쥴링이 발생하는 시점은?
  7. 선점형 스케쥴링과 비선점형 스케쥴링이란?
  8. 동시성이란?
  9. 병렬성이란?
  10. 데드락이란?
  11. 데드락의 4가지 조건은?
  12. 데드락의 해결방법은?
  13. 임계 영역이란?
  14. 운영체제에서의 기아란?
  15. 세마포어와 뮤텍스의 차이점은?
  16. 페이징과 세그먼트의 차이점은?
  17. 내부 단편화와 외부 단편화란?
  18. 페이지 교체 알고리즘이란?
  19. 컨텍스트 스위칭이란?
  20. 동기와 비동기의 차이점은?

 


 

답변

Q) 프로세스와 쓰레드의 차이점은?

더보기

프로세스는 자원을 할당받아 메모리 위에서 실행되고 있는 작업의 단위이고, 쓰레드는 프로세스가 할당한 자원에서 동작하고 있는 실행 단위입니다.

프로세스의 메모리 영역의 code, data, heap, stack 중 쓰레드는 stack만을 각자 할당받고 나머지는 공유합니다. 

따라서 쓰레드는 즉시 다른 쓰레드들의 결과를 확인할 수 있는데 반해, 프로세스는 다른 프로세스와 독립적인 구조로, 서로 통신하기 위해서는 IPC (프로세스 간 통신, Inter-Process Communication) 사용해야합니다.


 

Q) 메모리 영역 중 힙영역과 스택영역의 차의점은?

더보기

메모리 영역에는 code, data, heap, stack이 있습니다. 코드는 실행할 코드가 저장되는 텍스트 영역, 데이터는 전역변수와 정적변수가 저장되는 영역, 스택은 함수의 호출과 관계되는 지역변수와 매개변수가 저장되는 영역, 힙 영역은 사용자가 직접 관리할 수 있는 동적 메모리 영역입니다.

이 중 힙 영역은 런타임에 크기가 결정되고 스택영역은 컴파일타임에 크기가 결정됩니다.


 

Q) 쓰레드에서 stack을 분리한 이유는?

더보기
Stack은 함수의 호출 정보가 저장되는데, stack의 LIFO 구조로 인해 이를 공유하게되면 실행 순서가 꼬일 수 있기 때문입니다.

 

Q) 멀티 프로세싱과 멀티 쓰레딩의 장단점 및 차이점은?

더보기

멀티 프로세싱은 하나의 프로그램을 여러개의 프로세스로 구성하는 것을 의미합니다. 하나의 프로세스가 죽더라도 다른 프로세서에게는 영향을 끼치지 않는다는 장점이 있으며 컨텍스트 스위칭을 위한 오버헤드가 발생한다는 단점이 있습니다.

멀티 쓰레딩은 하나의 프로그램을 여러개의 쓰레드로 구성하는 것을 의미합니다. 컨텍스트 스위칭을 위한 오버헤드가 작다는 장점이 있고, 하나의 쓰레드에 문제가 생기면 다른 쓰레드에도 문제가 생긴다는 점과 자원 공유, 즉 동기화에 문제가 생길 수 있다는 점이 단점입니다.


 

Q) 멀티 쓰레드 환경에서의 주의점

더보기
다수의 쓰레드가 하나의 공유 자원에 접근할 때 상호 배제를 제거해 데드락을 예방하고 동기화 기법을 이용해 동시성 문제가 생기지 않도록 주의해야합니다.

 

Q) CPU 스케쥴링이 발생하는 시점은?

더보기
  • 실행상태에서 대기상태로 전환될 때 (예, 입출력 요청) - Non preemptive(비선점)
  • 실행상태에서 준비상태로 전환될 때 (예, 인터럽트 발생) - preemptive(선점)
  • 대기상태에서 준비상태로 전환될 때(예, 입출력이 종료될 때)
  • 종료될 때(Terminated)

 

Q) 선점형 스케쥴링과 비선점형 스케쥴링이란?

더보기
선점형 스케쥴링은 하나의 프로세스가 실행중인 다른 프로세스에게서 CPU를 뺏어올 수 있음을 의미하고 비선점형은 하나의 프로세스가 끝나지 않으면 CPU를 사용할 수 없음을 의미합니다.

 

Q) 동시성이란?

더보기
싱글 코어에서 멀티 스레드를 동작 시키기 위한 방법으로 멀티 태스킹을 위해 스레드들이 코어를 번갈아가며 사용하는 것을 의미합니다.

 

Q) 병렬성이란?

더보기
멀티 작업을 위해 멀티 코어에서 한 개 이상의 쓰레드를 포함하는 각 코어들을 동시에 실행하는 것을 의미합니다. 

Q) 데드락이란?

더보기
데드락, 또는 교착 상태란, 둘 이상의 프로세스들이 자원을 점유한 상태에서 서로 다른 프로세스가 점유하고 있는 자원을 요구하며 무한정 기다리는 상황을 말합니다. 예를 들면 A프로세스가 1번 자원을, B프로세스가 2번 자원을 점유하고 있는 상황에서 A는 2번 자원이, B는 1번 자원이 필요할 때 생길 수 있습니다.

 

Q) 데드락의 4가지 조건은?

더보기
  • 비선점 (Nonpreemptive) : 다른 프로세스의 자원을 뺏을 수 없음.
  • 순환 대기 (Circular wait) : 두 개 이상의 프로세스가 자원 접근을 기다릴 때, 관계가 순환적 구조.
  • 점유 대기 (Hold & Wait) : 공유 자원에 대한 접근 권한을 가진 채로 다른 자원에 대한 접근 권한을 요구.
  • 상호 배제(Mutual Exclusion) : 한 번에 한 프로세스만 공유 자원에 접근 가능하며, 접근 권한이 제한적일 경우.

 

Q) 데드락의 해결방법은?

더보기
데드락을 해결하기 위해서는 데드락의 4가지 조건 중 하나라도 없애면 됩니다. 공유 자원 중 많은 경우가 한 프로세스만 사용할 수 있는 경우가 많기 때문에 상호 배제는 없애기 힘들기에 많은 경우 순환 대기를 막는데 초점이 맞추어져있습니다. 해결 방법은 예방, 회피, 탐지와 복구 등이 있습니다.

 

Q) 임계 영역이란?

더보기
임계 영역이란 프로세스간의 공유 자원을 접근함에 있어 문제가 생기지 않도록 한번에 한 프로세스만이 접근하도록 해야하는 영역을 의미합니다. 임계 영역을 만들기 위해서는 세가지 조건이 필요합니다. 하나의 프로세스만이 들어갈 수 있어야 한다는 상호 배제, 임계 영역에 들어간 프로세스가 없는 상태에서 여러 프로세스가 동시에 임계 영역에 진입하려 할 때 어느 것이 들어갈지 결정해준다는 진행, 그리고 기아를 방지하기위한 한정 대기입니다.

 

Q) 운영체제에서의 기아란?

더보기
운영체제에서 기아란 우선 순위가 낮은 프로세스가 계속해서 자원을 할당 받지 못하는 것을 의미합니다.

 

Q) 세마포어와 뮤텍스의 차이점은?

더보기
뮤텍스는 lock을 사용해 공유 자원에 하나의 프로세스 또는 스레드만이 접근할 수 있도록 하는 것입니다. 그에 반해 세마포어는 세마포어의 카운트 갯수만큼의 프로세스 또는 스레드가 공유 자원에 접근할 수 있도록 하는 것입니다. 따라서 세마포어의 카운트를 1로 설정하면 뮤텍스로 사용이 가능합니다.

 


Q) 페이징과 세그먼트의 차이점은?

더보기

페이징은 프로세스를 물리적 고정 크기로 나누어 메모리에 불연속적으로 적재하는 방식을 뜻합니다. 페이징을 사용하면 외부 단편화 문제를 해결할 수 있으나 내부 단편화가 존재할 수 있습니다.

세그먼트는 프로세스를 논리적 내용으로 프로세스를 나누어 메모리를 할당하는 방식을 뜻합니다. 각 세그먼트는 크기가 다르며 내부 단편화가 존재하지 않으나 외부 단편화가 생길 수 있습니다.


 

Q) 내부 단편화와 외부 단편화란?

더보기
내부 단편화란 할당된 메모리 공간보다 프로세스가 작아서 사용하지 않는 메모리가 생기는 것을 의미합니다. 외부 단편화란 전체적으로 남아있는 메모리 공간은 프로세스를 올릴 수 있을 정도이지만 그 공간이 연속적이지 않아서 프로세스를 올릴 수 없는 상황을 의미합니다.

 

Q) 페이지 교체 알고리즘이란?

더보기
페이징 기법으로 관리되는 운영체제에서 주기억 장치에 필요한 페이지가 적재되어있지 않았을 때 어떤 페이지 프레임을 선택해 교체할지 선택하기 위해 사용하는 알고리즘입니다. FIFO, optimal 페이지 교체, LRU (Least Recently Used), LFU (Least Frequently Used), MFU (Most Frequently Used) 등이 있습니다. Optimal 페이지 교체는 실제로 활용할 수 있는 방법은 아니기에 연구 목적으로만 사용되며 LFU와 MFU는 구현 비용이 높기 때문에 실제로 쓰이지 않습니다.

 

Q) 컨텍스트 스위칭이란?

더보기
컨텍스트 스위칭이란 어떤 프로세스를 실행하고 있는 중에 발생한 인터럽트로 인해 현재 실행 중인 프로세스를 중지하고 다음 우선 순위 프로세스를 실행하기 위한 과정입니다. 현재 실행중인 프로세스의 상태, 즉 PCB를 저장하고 다음 프로세스를 동작시켜 작업을 처리한 후에 이전에 저장했던 컨텍스트를 복구시킵니다.

 

Q) 동기와 비동기의 차이점은?

더보기

동기는 요청과 결과가 동시에 일어난다는 의미로, 메소드를 실행시킴과 동시에 값이 반환되기 전까지는 blocking되어 있다는 것을 의미합니다. 비동기의 경우, blocking 되지 않고 이벤트 큐에 넣거나 백그라운드 스레드에게 해당 task를 위임하고 바로 다음 코드를 실행합니다.

동기 방식은 설계가 간편하고 직관적이라는 장점이 있으나 결과가 주어질때가지 대기해야한다는 단점이 있습니다. 비동기 방식은 동기보다 복잡하지만 결과가 주어질 때까지 다른 작업을 수행할 수 있기 때문에 자원을 효율적으로 사용할 수 있습니다.


 

'CS > 면접준비' 카테고리의 다른 글

네트워크  (0) 2024.08.17
데이터베이스  (0) 2024.08.17
JAVA  (0) 2024.08.17

질문모음

더보기
  1. 객체지향(OOP)이란?
  2. 객체지향 설계 원칙인 SOLID란?
  3. 관점지향(AOP)이란?
  4. 추상화란?
  5. 추상 클래스와 인터페이스의 특징과 차이를 설명하시오
  6. 캡슐화란?
  7. Spring Boot의 어노테이션 중 @Getter와 @Setter의 무분별한 사용을 지양해야하는 이유는 무엇인가?
  8. 상속이란?
  9. IS-A 관계와 HAS-A 관계의 차이점이란 무엇인가?
  10. 다형성이란?
  11. 자바의 컴파일 과정을 설명하시오
  12. JVM (Java Virtual Machine) 의 역할은 무엇인가?
  13. JVM의 내부 구조는?
  14. 자바의 메모리 영역의 종류와 그 특성은?
  15. 가비지 컬렉션을 위해 만들어진 heap 영역의 구조와 가비지 컬렉션의 동작방식은?
  16. Java 7의 특징은?
  17. Java 8의 특징은?
  18. Java 11의 특징은?
  19. 제네릭이란?
  20. String, StringBuilder, StringBuffer의 차이점은?
  21. String을 선언할 때 따옴표를 사용하는 것과 new를 사용하는 것의 차이점은?

 

답변

1) 객체지향(OOP)이란?

더보기

프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법입니다.


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

더보기
  • SRP (Single Responsibility Principle): 단일 책임 원칙, 하나의 클래스는 하나의 책임만을 가져야합니다
  • OCP (Open/Closed Principle): 개방 폐쇄 원칙, 확장에는 열려있고 수정에는 닫혀있어야합니다
  • LSP (Liskov Substitution Principle): 리스코브 치환 원칙, 상위 타입은 하위 타입으로 치환이 가능해야합니다
  • ISP (Interface Segregation Priciple): 인터페이스 분리 원칙, 하나의 범용 인터페이스보다 다수의 특정 인터페이스가 낫습니다
  • DIP (Dependency Inversion Principle): 의존 역전 원칙, 추상화에 의존해야지 구체화에 의존해선 안됩니다. 즉, 인터페이스, 추상 클래스와 같이 자주 변하지 않는 클래스와 관계를 맺어야합니다.

3) 관점지향(AOP)이란?

더보기

OOP를 더 발전시킨 개념으로 OOP에서 비즈니스 로직을 모듈화했다면 거기서 관점을 다르게해 인프라/부가 기능 측면에서 공통된 요소를 추출하는 것입니다. 예를들면 OOP가 프로그램을 계좌이체/입출금/이자계산으로 모듈화 했다면 AOP는 그 중에서 공통적으로 발생하는 부가 기능인 인증, 권한 체크, 트랜잭션 등을 분리시켜 필요한 시점에 자동으로 삽입되게 하는 것입니다.

이를 통해 코드 재활용성을 높이고 유지보수를 더 쉽게할 수 있습니다.

 


 

4) 추상화란?

더보기

추상화란 서로 다른 객체들의 특성 중 공통적인 속성, 동작들을 추출하여 하나의 묶음으로 정의하는 것을 의미합니다. 추상화를 이용한 설계는 코드의 재사용성을 높이고 그로 인해 간결해진 코드로 생산성, 가독성, 유지 보수 시간에 긍정적인 영향을 미칩니다

5) 추상 클래스와 인터페이스의 특징과 차이를 설명하시오

더보기

추상클래스는 일반 클래스와 별 다를 것이 없습니다. 단지, 추상 메서드를 선언하여 상속을 통해서 자손 클래스에서 완성하도록 유도하는 클래스입니다. 그래서 미완성 설계도라고도 표현합니다. 상속을 위한 클래스이기 때문에 따로 객체를 생성할 수 없습니다.

추상클래스가 미완성 설계도라면 인터페이스는 기본 설계도라고 할 수 있습니다. 인터페이스도 추상클래스처럼 다른 클래스를 작성하는데 도움을 주는 목적으로 작성하고 인터페이스는 추상메서드만을 가지고있습니다. 클래스와 다르게 다중상속이 가능합니다.

추상 클래스와 인터페이스를 나눠서 사용하는 이유는 사용 의도와 공통 기능 재사용이라는 두가지가 있습니다. 추상 클래스는 '상속', 즉 is-a 의 관계가 되기 때문에 클래스의 구분을 추상 클래스로 해결하고, 다중 상속이 가능한 인터페이스는 has-a 관계로 생각해 클래스가 가지고있는 기능들에 대한 부분을 해결하는 용도로 쓰입니다. 또한 만약 모든 클래스가 인터페이스를 사용해서 기본 틀을 구성한다면 공통으로 필요한 기능들도 모든 클래스에서 오버라이딩 하여 재정의 해야하는 번거로움이 있습니다.

6) 캡슐화란?

더보기

캡슐화란 객체 내부의 구조 및 데이터를 하나의 캡슐로 감싸 외부로부터 은닉하여 보호하는 것을 의미합니다. 캡슐화를 통해 높은 응집도 낮은 결합도를 유지할 수 있게됩니다.

  • 높은 응집도란 모듈 내의 요소들이 서로 밀접한 관련이 있어 하나의 목표를 위해 긴밀하게 협력하도록 설계되는 것입니다.
  • 낮은 결합도란 낮은 의존성을 나타내며, 즉 다른 모듈에 대한 지식을 얼만큼 가지고있는지를 나타내는 척도입니다.

7) Spring Boot의 어노테이션 중 @Getter와 @Setter의 무분별한 사용을 지양해야하는 이유는 무엇인가?

더보기

Getter와 Setter의 사용은 캡슐화를 위반하는 행위이기 때문입니다. 캡슐화는 객체의 상태는 숨기고 행동만을 외부에 공개해야합니다. 그렇기에 객체의 상태를 변경하거나 조회할 수 있는 Getter와 Setter은 캡슐화를 위반하는 것이 됩니다.

8) 상속이란?

더보기

상속이란 부모 클래스를 상속하여 기능을 추가하거나 재정의한 자식 클래스를 만드는 것을 의미합니다. 상속을 통해 코드의 중복을 없앨 수 있고 클래스 사이에 계층적인 관계를 구성해 다형성의 문법적 토대를 만듭니다

 

9) IS-A 관계와 HAS-A 관계의 차이점이란 무엇인가?

더보기

IS-A 관계는 'A는 B이다'와 같은 상속 관계를 나타냅니다. 이 정의에서 A는 상속을 받는 자식 클래스, B는 부모 클래스에 해당하며 예를들면 '사자는 동물이다'와 같이 사자가 완전히 동물에 종속되는 관계를 나타냅니다.

HAS-A 관계는 'A는 B를 가지고있다'와 같은 포함 관계를 나타냅니다. 이는 상속이 아닌 클래스가 다른 클래스를 소유하는 관계로, '컴퓨터는 CPU를 가지고있다'와 같이 컴퓨터 객체가 CPU 객체를 구성하 것과 같은 관계를 나타냅니다.

10) 다형성이란?

더보기

다형성이란 하나의 클래스나 함수가 다양한 방식으로 동작이 가능한 것을 의미합니다.

상속과 overriding을 통해 같은 타입의 객체더라도 다른 생성자로 생성된 객체는 동일한 이름의 함수를 다르게 동작시킬 수 있습니다.

또한 overloading을 통해 한 클래스 내에서 동일한 이름의 함수를 서로 다른 매개변수 타입과 개수를 가지게 해 동일한 이름이더라도 다양한 방식으로 동작할 수 있습니다.

 


11) 자바의 컴파일 과정을 설명하시오

더보기

개발자가 .java 소스파일을 작성합니다.

자바 컴파일러가 소스 파일을 컴파일해 JVM이 이해할 수 있는 .class 자바 바이트코드 파일을 생성합니다.

JVM내의 클래스 로더를 통해 바이트코드 파일을 JVM 메모리 내로 로드합니다.

JVM은 각 OS가 실행할 수 있는 기계어로 변환시킵니다

12) JVM (Java Virtual Machine) 의 역할은 무엇인가?

더보기

스택 기반으로 동작하는 가상 머신인 JVM은 자바 바이트 코드 파일을 OS에 맞게 해석해주는 역할과 가비지 컬렉션을 통해 메모리 관리를 해주는 역할을 가지고있습니다.

13) JVM의 내부 구조는?

더보기

JVM의 구조는 크게 garbage collector, execution engine, class loader, runtime data area가 있고 각자의 역할에 따라 동작합니다

  • Garbage collector는 heap 메모리 영역에서 동적으로 할당했던 메모리 중 유효하지 않은 메모리인 garbage를 주기적으로 정리해 주는 역할을 합니다.
  • Execution engine는 클래스 로더를 통해 JVM 내의 Runtime Data Area에 배치된 바이트 코드들을 명렁어 단위로 읽어서 실행합니다. 
  • Class loader은 JVM 내로 .class 파일을 로드하고, JVM이 운영체제로부터 할당 받은 Runtime Data Area에 적재합니다. 런타임 시에 동적으로 클래스를 로드합니다.
  • Runtime data area는 자바 어플리케이션을 실행할 때 사용되는 데이터들을 적재하는 데이터 영역으로, method, stack, heap, PC register, native method stack과 같은 다섯가지 영역으로 나누어져있습니다.

14) 자바의 메모리 영역의 종류와 그 특성은?

더보기

자바의 메모리 공간은 method, stack, heap, PC register, native method stack 영역으로 구분되고 데이터의 타입에 따라 적절한 영역에 할당됩니다.

  • Method 영역에는 전역 변수, static으로 선언되는 것들을 저장하며, 클래스 정보, static 변수, 변수 정보, 메소드 정보, 런타임 상수풀등을 저장합니다. 프로그램의 시작부터 종료까지 메모리에 남아있으며 모든 쓰레드가 공유합니다. 그렇기 때문에 이 영역에 저장되어있는 데이터들을 어디서든 사용할 수 있게 됩니다. JVM이 동작해서 클래스가 로딩될 때 할당됩니다.
    • 런타임 상수풀: JVM은 런타임 상수풀에서 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조합니다
  • Heap 영역에는 Object 타입의 데이터 배열을 저장합니다. 가비지 컬렉터가 주기적으로 관리하는 영역이기도합니다. Compile time시 할당되며 이 또한 모든 쓰레드가 공유합니다.
  • Stack 영역은 메서드 호출 시마다 스택 프레임(그 메서드만을 위한 공간)의 형태로 생성됩니다. 그리고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장합니다. 만약, 지역변수이지만 Reference Type일 경우에는 Heap 에 저장된 데이터의 주소값을 Stack 에 저장해서 사용하게 됩니다. 마지막으로, 메서드 수행이 끝나면 프레임별로 삭제하게되고 쓰레드 당 하나씩 존재합니다.
  • PC register 영역은 쓰레드가 생성될 때마다 생성되는 영역으로 프로그램 카운터, 즉 현재 스레드가 실행되는 부분의 주소와 명령을 저장하고 있는 영역입니다.
  • Natvie method stack 영역은 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역입니다. JNI(Java Native Interface)를 통해 호출되는 다른 언어의 코드를 수행하기 위해 존재합니다.

15) 가비지 컬렉션을 위해 만들어진 heap 영역의 구조와 가비지 컬렉션의 동작방식은?

더보기

Heap 영역은 young generation, tenured generation으로 나누어져있으며 해당 영역은 application에서 사용됩니다. Java 7까지는 이에 추가로 JVM이 사용하는 permanent generation영역이 존재했으나 Java 8부터는 OS가 관리하는 native 영역의 메모리의 Metaspace로 바뀌었습니다.

Young generation은 또다시 eden영역과 survivor 영역으로 나누어져있습니다. Heap영역에 객체가 새로 생성되면 eden에 저장이 되고 eden이 어느정도 차면 참조 정도에 따라 survivor 영역의 빈 공간으로 이동됩니다.

Tenured generation에는 old영역이 있으며 young generation 영역이 차게되면 참조 정도에 따라 이 영역으로 옮겨집니다. 이 과정은 minor garbage collection이라고 불립니다. Old 영역의 허용치가 넘어가면 old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 major GC가 진행됩니다. Major GC가 진행되는 중에는 GC를 진행하는 쓰레드를 제외한 모든 쓰레드가 멈추는 'stop-the-world'가 발생합니다.


16) Java 7의 특징은?

더보기
  • Try-with-resources 지원: try문 안에서 사용되는 Connection, Files, Input/OutStream 등과 같은 자원들을 자동적으로 해제를 할 수 있게 되었습니다.
    • try (final InputStream inputStream = connection.getInputStream();
                   final OutputStream outputStream = connection.getOutputStream()){ /*code*/ }
  • 다이아몬드 연산자를 활용한 Type Reference(타입 추론)지원: 생성자 호출시에 필요한 파라미터(매개변수)를 다이아몬드 연산자(<>)을 사용함으로 생략할 수 있습니다.
    • Map<String, List<String>> myMap = new HashMap<>();
  • 다중 Exception Catching: 하나의 catch 블록에서 여러개의 예외처리가 가능합니다.
    • try{ /*code*/ } catch (FirstException | SecondException ex) {
           log.info(ex);
           throw ex;
      }
  • 숫자 리터럴에서 언더스코어(underscore) 지원: 숫자 리터럴의 숫자 사이에 언더스코어(_)를 사용할 수 있습니다. 이 구분자를 사용하면 의미 있는 숫자끼리 그룹화하는 것이 가능하며, 이로 인해 코드의 가독성이 높아집니다.
    • int oneMillion = 1_000_000;
  • Switch문에서 String 객체 사용가능

17) Java 8의 특징은?

더보기
  • Heap Permanent Generation 제거: 이전에는 permanent generation이라는 이름을 가지고 초기 설정시 설정해여야했던 메모리가 metaspace로 변경되었습니다. Metaspace는 런타임시 메모리 요구 사항에 따라 다이나믹하게 자체 크기 조절이 가능하며 OS레벨에서 관리되는 native 메모리 영역에 위치합니다. 
  • Interface에 Default, Static Methods를 추가할 수 있게됨: default method는 하위 클래스에서 재정의가 가능하지만 static method는 불가합니다.
  • 함수형 interfaces: 하나의 기능을 제공하는 단 하나의 추상 메소드를 정의하는 인터페이스가 추가되었습니다. 단, 추상 메소드 외에 디폴트 메소드나 정적 메소드는 원하는 만큼 사용할 수 있고, @FunctionalInterface와 함께 쓰이며, 만약 추상 메소드가 여러 개라면 컴파일 타임에서 에러를 잡아낼 수 있습니다. 주요 함수형 인터페이스로는 Consumer, Supplier, Function 등이 있습니다.
  • 람다 표현식: 람다 표현식은 함수를 식으로 나타내는 것으로 함수를 변수로 나타낼 수 있다는 장점이 있습니다.이러한 람다 표현식은 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높이는 데 그 목적이 있습니다. Java 8 버전부터는 람다 표현식을 사용하여 자바에서도 함수형 프로그래밍을 할 수 있게 되었습니다. 
  • 메서드 참조: 람다식의 가독성을 높이기 위해 사용하는 방법입니다. 종류로는 정적 메서드참조, 인스턴스 메서드 참조, 특정 유형의 객체의 인스턴스 메서드 참조, 생성자 참조 등이 있으며 모두 더블콜론을 사용하여 사용됩니다.
  • Stream API: 스트림은 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해 주는 내부 반복자입니다. 스트림 API는 데이터를 추상화하여 다루므로, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공합니다. 
    • 컬렉션은 데이터를 어떻게 저장/관리하고 접근하는지를 목표하고 있습니다.
      이와 반대로 스트림은 데이터를 직접 접근하거나 조작하는 기능을 제공하지 않으며, 데이터를 어떻게 계산할지에 대한 목표를 가지고있습니다.
      즉, 데이터의 저장/관리가 목적이라면 컬렉션을, 데이터의 계산이 목적이라면 스트림을 사용하는게 좋다고 볼 수 있습니다.
  • Date and time API 지원 
  • Optional 지원: null에 대한 참조를 안전하게할 수 있게됩니다.
  • 배열 정렬의 병렬처리: 이전의 Arrays.sort는 merge sort와 같은 순차적 실행이 되는 방식을 사용했으나 Arrays.parallelSort가 추가되며 병렬처리가 가능해졌습니다.
  • Base64 encoding and decoding 지원

18) Java 11의 특징은?

더보기
  • String, Files 클래스와 Collection 인터페이스에 새로운 메서드가 추가되었습니다.
  • 람다에서 로컬변수인 var을 이용할 수 있게되었습니다.
  • javac를 통해 컴파일하지 않고 바로 컴파일할 수 있게되었습니다
  • Default garbage collector이 parallelGC에서 G1GC로 변경되었으며 G1GC는 더 좋은 성능을 냅니다.

19) 많은 기업이 아직 Java 8을 사용하는 이유는?

더보기

Java 8보다 나중에 나온 LTS인 Java 11과 Java 17보다도 더 긴 지원기간을 가지고있습니다.


20) 제네릭이란?

더보기

데이터 타입을 일반화 하는 것으로, 데이터 타입에 대한 정의를 외부로 미루는 것입니다.

21) String, StringBuilder, StringBuffer의 차이점은?

더보기

String은 불변하는 특성을 가지고있기 때문에 수정이 불가합니다. 따라서 수정이 발생할때마다 새로운 인스턴스가 생성되고 이전 값들은 가비지 컬렉션 발생 시 삭제됩니다. StringBuffer은 가변하는 특성을 가지고있으며 동기화를 지원하기 때문에 멀티스레드 환경에서 안전합니다. StringBuilder 또한 가변하는 특성을 가지고있으나 동기화를 지원하지 않기에 멀티스레드 환경에서 사용하기에 적합하지 않으나 단일 스레드 환경이라면 StringBuffer보다 높은 성능을 보입니다.

22) String을 선언할 때 따옴표를 사용하는 것과 new를 사용하는 것의 차이점은?

더보기

String은 primitive type가 아니기 때문에 heap에 저장됩니다. 그러나 java는 String 객체를 다른 객체들과 달리 heap 메모리 영역 안의 String constant pool에서 관리합니다. 만약 따옴표로 선언이 되면 이 String 상수풀에서 해당 값을 가진 String이 있는지 확인하고 있다면 그 주소값을 리턴하고, 없다면 String 상수풀에 해당 객체를 생성, 할당한 뒤 그 주소값을 리턴합니다. 이를 통해서 같은 값을 가진 String 객체가 생성되는 것을 막을 수 있습니다.

만약 new를 사용한다면 다른 객체들처럼 강제로 heap 영역에 객체를 생성합니다. 그러나 이럼에도 다른 객체와 다르게 동작하는데, 만약 new를 사용했다 하더라도 String 상수풀에 그 값이 있는지 확인하고 없다면 String 상수풀에도 객체를 생성하여 총 두개의 객체를 생성합니다.

 

'CS > 면접준비' 카테고리의 다른 글

네트워크  (0) 2024.08.17
데이터베이스  (0) 2024.08.17
운영체제  (0) 2024.08.17

Oracle을 이용해 프로그래머스 레벨1 문제를 모두 풀며 잊지 않기 위한 내용을 정리합니다. 문제링크는 생략합니다.

 

 

1. 상위 N개 행 조회하기

ROWNUM을 이용하는 방법.

SELECT NAME
FROM (SELECT NAME FROM ANIMAL_INS ORDER BY DATETIME)
WHERE ROWNUM <= 1;

 

2. NULL 처리

NVL : NULL값이 있으면 다른 NULL이 아닌 지정값으로 조회되도록 하는 함수

SELECT warehouse_id, warehouse_name, address, NVL(freezer_yn, 'N')
from food_warehouse
where address like '경기도%';

 

-- 코드를 입력하세요
SELECT PT_NAME, PT_NO, GEND_CD, AGE, NVL(TLNO, 'NONE')
FROM PATIENT
WHERE AGE <= 12 AND GEND_CD = 'W'
ORDER BY AGE DESC, PT_NAME;

 

3. 날짜처리

TO_CHAR : 날짜 데이터와 형식을 인자로 넘기면 형식에 맞추어 출력해주는 함수

SELECT COUNT(*) AS USERS
FROM USER_INFO
WHERE TO_CHAR(JOINED, 'YYYY') = '2021' AND AGE >=20 AND AGE <= 29;

 

-- 코드를 입력하세요
SELECT dr_name, dr_id, mcdp_cd, to_char(hire_ymd, 'yyyy-mm-dd')
from doctor
where mcdp_cd IN ('CS', 'GS')
order by hire_ymd desc, dr_name asc;

 

 

4. 기본 JOIN

아직 JOIN에 대해 깊이있게 이해를 못해서 추가적으로 공부가 필요합니다.

SELECT H.FLAVOR
FROM FIRST_HALF H
INNER JOIN ICECREAM_INFO I ON H.FLAVOR = I.FLAVOR
WHERE H.TOTAL_ORDER > 3000 AND I.INGREDIENT_TYPE = 'fruit_based';

 

SELECT B.TITLE,
       B.BOARD_ID,
       R.REPLY_ID,
       R.WRITER_ID,
       R.CONTENTS,
       TO_CHAR(R.CREATED_DATE, 'YYYY-MM-DD') AS CREATED_DATE
FROM USED_GOODS_BOARD B
INNER JOIN USED_GOODS_REPLY R ON B.BOARD_ID = R.BOARD_ID
WHERE TO_CHAR(B.CREATED_DATE, 'YYYYMM') = '202210'
ORDER BY R.CREATED_DATE, B.TITLE;

 

 

5. 평균, 반올림

AVG는 평균을 구해주며

ROUND는 반올림한 값을 줍니다. ROUND의 두번째 인자값에 따라 반올림 위치를 지정할 수 있습니다.

두번째 인자를 적지 않는다면 0으로 인식하며 소수점 첫째짜리에서 반올림한 값을 리턴합니다.

SELECT ROUND(AVG(DAILY_FEE)) AS AVERAGE_FEE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE = 'SUV';

 

 

6. CASE-WHEN-THEN

주어진 테이블의 정보를 통해 새로운 컬럼을 만들고 싶을때 사용하는 것으로 이해했습니다.

조건이 여러개라면 WHEN-THEN-WHEN-THEN...형태로 이어붙이면 되는것 같습니다.

-- 코드를 입력하세요
SELECT HISTORY_ID, CAR_ID, TO_CHAR(START_DATE, 'YYYY-MM-DD') AS START_DATE, TO_CHAR(END_DATE, 'YYYY-MM-DD') AS END_DATE,
CASE WHEN(END_DATE - START_DATE + 1) >= 30 THEN '장기 대여' ELSE '단기 대여' END AS RENT_TYPE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE TO_CHAR(START_DATE, 'YYYY-MM') = '2022-09'
ORDER BY HISTORY_ID DESC;

 

 

 

옵저버 패턴

정의

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자 객체들이 관찰하는 객체에 변화가 있을 때마다 변경 정보를 받고 갱신되도록 하는 디자인 패턴입니다.

옵저버 패턴은 1:1혹은 1:N 의존성을 가지는데, 주로 분산 이벤트 처리 시스템에서 많이 사용됩니다.

발행/구독 모델로 알려져 있기도 합니다.

객체 사이의 의존성을 줄이고 유연하게 상호작용하도록

즉, Coupling을 느슨하게 유지하는 것을 도와주는 패턴입니다.

이해하기 가장 쉬운 예시는 유튜브 혹은 인스타그램을 생각하면 됩니다.

유튜브에서 내가 구독한 채널이 새로운 영상을 올리면 새 영상 업로드 알림을 받습니다.

나 뿐만아니라 같은 채널을 구독한 다른 모든 구독자들도 알림을 받을것입니다.

옵저버 패턴에서 유튜브 채널이 Subject가 되고, 구독자들이 Observer가 됩니다

 

관찰자(Observer)라고 해서 관찰자 객체들이 Subject를 스스로 관찰하는 것처럼 느껴지지만
직접 관찰한다는 느낌 보다는 Subject의 변화를 통한 정보 갱신을 하기 위해 변경사항을 전달받길 기다리는 수동적인 상태에 더 가깝습니다.

 

옵저버 패턴의 클래스 다이어그램을 그리면 아래와 같습니다.

Subject와 Observer는 인터페이스 이며 이것을 구현하는 클래스가 필요합니다.

Subject에서 observerCollection을 갖는데 타입으로 L등이 될 수 있습니다.

Subject를 구현한 클래스를 ConcreteSubjet라고 하겠습니다.

ConcreteSubjets는 Observer들을 List, Set, Map 등의 Collection에 모아 갖고 있습니다.

또한 Observer들을 내부에서 등록 및 삭제하는 구조이며 Subject의 상태가 변경되면 Observer들에게 알림을 발행합니다.

 

 

옵저버 패턴 흐름

1. 한개의 주체(Subject)와 여러개의 관찰자(Observer)로 구성되어습니다.

2. Observer 패턴에서는 Subject의 상태가 바뀌면 변경사항을 Obsever들에게 전달합니다.

3. Subject로부터 받은 정보로 Observer의 정보를 바꿀 수 있습니다.

4. Observer들은 Subject의 Collection에서 삭제/추가 될 수 있습니다.

유튜브 채널이 있고, 영상을 게시했을때 구독자에게 알림을 보내는 상황을 가정하여 코드를 작성해봤습니다.

 

Observer.java

public interface Observer {
    void update(String title);
}

관찰자/구독자 역할을 하게될 Observer 인터페이스입니다.

ObserverImpl.java

public class ObserverImpl implements Observer{
    String name;
    public ObserverImpl(String name){
        this.name = name;
    }
    @Override
    public void update(String title) {
        System.out.println(this.name + "님, [" + title + "] 영상이 게시되었습니다.");
    }
}

Observer를 구현한 클래스입니다.

 

Subject.java

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObserver(String title);
}

 

SubjectImpl.java

import java.util.ArrayList;
import java.util.List;

public class SubjectImpl implements Subject{

    private final List<Observer> observerList;
    public SubjectImpl(){
        observerList = new ArrayList<>();
    }

    // Observer 추가
    @Override
    public void registerObserver(Observer o) {
        observerList.add(o);
    }

    // Observer 삭제
    @Override
    public void removeObserver(Observer o) {
        observerList.remove(o);
    }

    // Observer들에게 이벤트 알림
    @Override
    public void notifyObserver(String title) {
        for (Observer o : observerList)
            o.update(title);
    }
}

 

Subject를 구현한 클래스입니다.

Observer들을 List를 이용해 관리합니다.

옵저버를 등록하고 알림을 보내는 것을 간단하게 구현했습니다.

public class youtube {
    public static void main(String[] args) {
        // 유튜브 채널 생성
        Subject channel = new SubjectImpl();
        
        // 구독자 생성
        Observer subscriber1 = new ObserverImpl("철수");
        Observer subscriber2 = new ObserverImpl("영희");
        
        // 구독자 등록
        channel.registerObserver(subscriber1);
        channel.registerObserver(subscriber2);
        
        // 영상 업로드 알림
        channel.notifyObserver("웃긴 짤");
        channel.notifyObserver("무서운 짤");
        
        // 영희가 구독 취소
        channel.removeObserver(subscriber2);
        
        // 영상 업로드 알림
        channel.notifyObserver("슬픈 짤");
    }
}

 

 

실행 결과는 아래와 같습니다.

영희가 구독을 취소한 이후로는 영희에게 알림이 가지 않습니다.

 

 

 

장점

1. 옵저버 패턴을 사용하면 Subject의 상태 변경을 매번 일일히 확인하지 않고도 자동으로 감지가 가능합니다.

2. Subject의 코드를 변경하지 않아도 새 구독자 클래스를 도입이 가능해 OCP를 준수합니다.

3. 런타임 시점에서 Subject와 Observer가 구독 알림 관계를 맺을 수 있습니다.

4. Subject와 Observer의 관계를 느슨하게 유지할 수 있습니다.

단점

1. 알림의 순서를 제어할 수 없으며, 무작위 순서로 알림을 받게됩니다.

2. 무분별한 사용은 코드의 복잡도가 증가합니다.

3. 옵저버 객체 등록 이후 해지하지 않으면 메모리 누수가 발생할 수 있습니다.

 


MVC 패턴

정의

MVC 패턴은 사용자 인터페이스를 설계하는 데 사용되는 디자인 패턴으로, 애플리케이션을 세 가지 주요 구성 요소로 나눕니다.

1. Model: 애플리케이션의 데이터와 비즈니스 로직을 처리합니다.

2. View: 사용자 인터페이스 요소를 처리하고 모델의 데이터를 사용자에게 표시합니다.

3. Controller: 사용자 입력을 처리하고 모델과 뷰를 업데이트합니다.

옵저버 패턴은 MVC 패턴의 모델과 뷰 간의 관계를 구현하는 데 자주 사용됩니다.

모델이 옵저버 패턴의 Subject이고, 뷰가 Observer가 됩니다.

모델의 상태가 변경되면, 모델은 뷰에게 변경 사항을 알리고, 뷰는 이를 반영하여 화면을 업데이트 합니다.

이것을 통해 MVC 패턴에서 모델과 뷰 간의 느슨한 결합을 유지할 수 있습니다.


옵저버 패턴을 직접 구현하지 않아도 자바의 내장 Observer 객체가 있어 클래스를 상속하기만 하면 쉽게 이용할 수 있습니다. (java.util.Observer, java.util.Observable)

하지만 Observable는 인터페이스가 아닌 클래스입니다. Observable클래스를 상속해야 하는데, 자바에서는 단일 상속만 지원하기 때문에

Subject역할을 하는 클래스가 다른 클래스를 상속하고 있는 상태라면 내장 객체를 이용할 수 없습니다.

메서드 위임을 해주면 되지 않나 싶지만, Observable의 메서드가 protected로 선언되어 있기 때문에 결국 자식 클래스에서 호출할 수 밖에 없습니다.

따라서 내장 객체를 이용할 수 없는 상황이라면 직접 구현해줘야만 합니다.

 

 

 

 

출처 및 참고

https://shan0325.tistory.com/33

https://gsbang.tistory.com/entry/Design-Pattern-Observer-Pattern%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4

https://velog.io/@te-ing/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%98%B5%EC%A0%80%EB%B2%84Observer-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

https://xzio.tistory.com/289

https://refactoring.guru/design-patterns/observer

https://ssdragon.tistory.com/144

 

'CS > JAVA' 카테고리의 다른 글

JVM 이해하기 (2) - Class Loader  (2) 2025.01.29
JVM 이해하기 (1) - 도입  (3) 2025.01.28

https://boj.ma/17943/t

 

17943번: 도미노 예측

 

boj.ma

 

${N}$과 ${Q}$, 그리고 ${N-1}$개의 수가 주어집니다.

${N - 1}$개의 수는

${a_0}$ ${\oplus}$ ${a_1}$, ${a_1}$ ${\oplus}$ ${a_2}$, .... , ${a_{n-1}}$  ${\oplus}$ ${a_n}$ 으로, 입력으로 주어진 i번째 숫자는 원본배열의 i번째 수와 i+1번째 수를 xor한 값입니다.

 

이후 ${Q}$개의 쿼리가 주어지는데, 쿼리의 종류가 두 가지 있습니다.

0 x y 는 x번째 수와 y번째 수를 xor한 값을 출력하는 쿼리이고,

1 x y d는 x번째 수가 d일때, y번째 수를 출력하라는 쿼리입니다.

 

xor문제에서는 항상 xor의 중요한 특성 두 가지를 떠올리면 풀리는 경우가 많습니다.

 

${0}$ ${\oplus}$ ${X}$ = ${X}$

 

${X}$ ${\oplus}$ ${X}$ = ${0}$

 

이 두가지 xor연산의 특징을 바탕으로 생각을 해봅니다.

 

0번 쿼리인 x번째 수와 y번째 수를 xor한 값의 결과는

주어진 배열에서

${A_x}$ ${\oplus}$ ${A_{x+1}}$ ${\oplus}$ ${A_{x+1}}$ ${\oplus}$ ${A_{x+2}}$  ${\oplus}$ ${...}$ ${\oplus}$ ${A_{y-2}}$ ${\oplus}$ ${A_{y-1}}$ ${\oplus}$  ${A_{y-1}}$ ${\oplus}$ ${A_y}$ 를 해주면 중간 항이 모두 소거되기 때문에

${A_x}$ ${\oplus}$ ${A_y}$ 와 같습니다.

 

따라서 입력으로 주어진 배열에서 x번째 수부터 y - 1번째 수까지 xor한 값을 출력해주면 됩니다.

 

1번 쿼리는

 

${X}$번째 수가 ${d}$일때 ${Y}$번째 수를 출력하는 것인데, ${A_X}$ ${\oplus}$ ${A_Y}$ ${\oplus}$ ${A_X}$ ${=}$ ${A_Y}$ 이므로

 

0번 쿼리를 그대로 갖고온 결과에 ${d}$ 를 xor 해주면 됩니다.

 

당연히 쿼리마다 일일히 xor해주면 시간초과이므로, 누적합 배열을 이용해 전처리 해주면 쿼리마다 ${O(1)}$ 에 값을 구할 수 있습니다.

 

코드(python)

import sys
input = sys.stdin.readline


n, q = map(int, input().split())
ls = [0] + list(map(int, input().split()))
k = len(ls)
pxor = [0] * (k)
for i in range(1, k):
    pxor[i] = pxor[i- 1] ^ ls[i]
for _ in range(q):
    oper, *a = map(int, input().split())
    if oper:
        x, y, d = a
        print(pxor[y - 1] ^ pxor[x - 1] ^ d)
    else:
        x, y = a
        print(pxor[y - 1] ^ pxor[x - 1])

 

 

'알고리즘 문제 풀이' 카테고리의 다른 글

백준 33543 둘이 한 팀  (0) 2025.03.03
백준 2001 보석 줍기  (2) 2024.08.31
백준 7346 유전자 함수  (0) 2024.05.16
백준 23889 돌 굴러가유  (0) 2024.05.16
백준 31786 최고의 친구  (0) 2024.05.09

제가 안까먹으려고 하나의 글에 모두 정리해둡니다.

아래 justicehui님의 splay tree글을 많이 참고하여 작성합니다.(사실상 정리)

https://justicehui.github.io/hard-algorithm/2018/11/12/SplayTree1/

https://justicehui.github.io/hard-algorithm/2018/11/13/SplayTree2/ 

https://justicehui.github.io/hard-algorithm/2019/10/22/SplayTree3/

https://justicehui.github.io/hard-algorithm/2019/10/23/SplayTree4/

 

 

Splay Tree?

스플레이 트리는 BBST의 한 종류로 삽입/삭제/검색 모두 amortized O(logN)에 해주는 자료구조이며 다른 BBST에 비해 구현이 쉽다는 특징이 있습니다. 또한 구간 뒤집기 연산도 가능합니다.

 

 

스플레이  트리 노드 구조체

struct node {
    node* l;
    node* r;
    node* p;
    int key;
}

왼쪽 자식, 오른쪽 자식, 부모 노드, key값을 갖도록 합니다.

 

 

Rotate

rotate 함수를 통해 특정 노드를 자신의 부모 노드와 자리를 바꾸도록 합니다.

출처 : https://justicehui.github.io/hard-algorithm/2018/11/12/SplayTree1/

 

위 그림처럼 X노드를 P노드와 자리를 바꾸고 트리의 형태에 따라 옮긴 후 노드간의 관계를 재설정 하도록하는 함수를 만들것입니다.

void rotate(node* x){ // 위로 올릴 노드
    node* p = x -> p; // x의 부모 노드
    node* b = NULL; // b노드
    if(!p) return; // x가 루트라면 리턴
    if (x == p->l){ // x가 p의 왼쪽 자식
        p -> l = b = x -> r; // p의 왼쪽을 b노드로 
        x -> r = p; // x의 오른쪽 자식을 p노드로
    }else{ // x가 p의 오른쪽 자식
        p -> r = b = x -> l; // p의 오른쪽을 b노드로
        x -> l = p; // x의 왼쪽 자식을 p노드로
    }
    // 부모노드 재설정
    x -> p = p -> p; // x의 부모는 p의 부모
    p -> p = x; // p의 부모는 x
    if (b) b -> p = p; // b노드가 존재한다면 b의 부모는 p
    
    // x가 루트노드가 아닐경우
    // p노드가 p노드의 부모노드의 왼쪽이었다면 x를 왼쪽으로, 오른쪽이었다면 오른쪽으로
    (x -> p ? p == x -> p -> l ?  x -> p -> l : x -> p -> r : tree) = x;
}

 

node* b 는 위 그림에서의 B노드입니다.

 

 

Splay

이제 Splay 함수를 구현할것입니다. Splay 함수는 특정 노드를 루트노드까지 올리는 작업을 하게 될것입니다.

 

함수는 다음과 같은 순서로 진행됩니다.

 

1. x가 루트라면 종료합니다.

2. x의 부모가 루트라면 rotate(x)를 하고 종료합니다. (Zig step)

3. x의 조부모(부모의 부모)를 g라고 할 때 상황에 따라 나누어집니다.

3- 1. g-p 방향과 p-x 방향이 같다면, rotate(p) -> rotate(x)를 수행합니다. (Zig-Zig step)

3 - 2. 방향이 다르다면, rotate(x)를 두 번 수행합니다. (Zig-Zag step)

 

이걸 x가 루트가 될 때까지 반복합니다.

 

zig, zig-zig, zig-zeg 과정은 아래 사진과 같이 진행됩니다.

 

1. zig step

2. zig-zig step

 

x와 p가 둘 다 왼쪽 자식이거나 둘 다 오른쪽 자식일 경우

rotate(p) 후에 rotate(x)를 해줍니다.

 

3. zig-zag step

 

함수로 구현하면 다음과 같습니다.

void splay(node* x){
    while(x->p){  // x가 루트가 될때까지 반복
        node* p = x -> p;
        node* g = p -> p;
        if (g){
            if ((x == p -> l) == (p == g -> l)) rotate(p); // zig-zig
            else rotate(x); // zig-zag;
        }
        rotate(x);
    }
}

 

 

이제 insert, find, delete 함수를 차례차례 보겠습니다.

 

Insert

삽입 방식은 일반적인 BST와 같고 마지막에 splay를 해서 루트로 올려줍니다.

void insert(int key){
    node* p =tree;
    node** pp; // 넣을 위치
    if(!p){ // 빈 트리인 경우
        node* x = new node;
        tree = x;
        x -> l = x -> r = x -> p = NULL;
        x -> key = key;
        return;
    }
    while(true){ // 삽입할 위치를 찾을때까지 반복
        if (key == p -> key) return; // 중복값이 있을 경우 종료
        if (key < p -> key){ // 현재 노드의 값보다 작을경우
            if (!p -> l){ // 왼쪽이 비어있으면 왼쪽에 삽입
                pp = &p->l;
                break;
            }
            p = p->l; // 비어있지 않으면 왼쪽 자식으로 이동
        }else{ // 현재 노드의 값보다 크다
            if (!p->r){
                pp = &p->r;
                break;
            }
            p = p->r;
        }
    }
    // 위에서 삽입할 위치를 찾음
    node* x = new node;
    *pp = x;
    x -> l = x -> r = NULL;
    x-> p = p;
    x-> key = key;
    // splay
    splay(x);
}

 

 

find

find도 또한 비슷하게 진행되며

마지막에 splay를 해주어 다른 연산을 쉽게 해주도록 합니다.

bool find(int key){
    node* p = tree;
    if(!p) return false; // 비어있는 트리
    while(p){
        if (key == p -> key) break; // 찾았다
        if (key < p -> key){ // 찾으려는게 현재 값보다 작음
            if (!p -> l) break; // 왼쪽이 없다 -> 탐색 실패
            p = p -> l; // 왼쪽으로 이동
        }else{ // 찾으려는게 현재 값보다 큼
            if (!p -> r) break; // 오른쪽이 없다 -> 탐색 실패
            p = p -> r; // 오른쪽으로 이동
        }
    }
    splay(p); // 마지막에 탐색한 노드를 루트로 올림
    return key == p->key; // 탐색한 노드의 값이 찾는값이랑 같은지 return
}

 

 

Delete

삭제연산입니다.

void del(int key){
    if (!find(key)) return; // 삭제할 값이 없으면 루트로 이동 후 종료
    node* p = tree;
    if (p -> l && p -> r){ // 자식이 두개
        tree = p->l; // 왼쪽 자식이 새로운 루트
        tree->p = NULL;

        // 오른쪽 서브트리를 왼쪽 서브트리 아래에 삽입
        node* x = tree;
        while(x -> r) x = x -> r;
        x -> r = p -> r;
        p -> r -> p = x;
        delete p;
        return;
    }

    if(p->l){ //자식이 왼쪽만 있는 경우
        tree = p->l;
        tree->p = NULL; //왼쪽 자식이 새로운 루트
        delete p; //노드 삭제
        return;
    }
    if(p->r){ //자식이 오른쪽만 있는 경우
        tree = p->r;
        tree->p = NULL; //오른쪽 자식이 새로운 루트
        delete p; //노드 삭제
        return;
    }

    // 노드가 자신 하나인경우
    delete p;
    tree = NULL;
}

 

 

K-th element

k번째 원소를 찾는 함수도 구현해봅시다

k번째 원소를 찾으려면 각 노드를 루트로하는 서브트리의 크기를 알아야 합니다.

구조체에 변수 하나를 추가해줍니다.

struct node{
    node* l;
    node* r;
    node* p;
    int key, cnt;
}

 

cnt변수의 값은 rotate가 될 때마다 계속 바뀔 것입니다.

cnt변수의 갱신을 담당할 update 함수를 만들어 줍니다.

void update(node* x){
    x -> cnt = 1;
    if (x -> l) x->cnt += x->l->cnt;
    if (x -> r) x->cnt += x->r->cnt;
}

 

rotate 할 때마다 cnt값이 바뀐다고 했으니 rotate의 마지막 부분에 update함수를 추가해주면 됩니다.

 

void rotate(node* x){ // 위로 올릴 노드
    node* p = x -> p; // x의 부모 노드
    node* b = NULL; // b노드
    if(!p) return; // x가 루트라면 리턴
    if (x == p->l){ // x가 p의 왼쪽 자식
        p -> l = b = x -> r; // p의 왼쪽을 b노드로
        x -> r = p; // x의 오른쪽 자식을 p노드로
    }else{ // x가 p의 오른쪽 자식
        p -> r = b = x -> l; // p의 오른쪽을 b노드로
        x -> l = p; // x의 왼쪽 자식을 p노드로
    }
    // 부모노드 재설정
    x -> p = p -> p; // x의 부모는 p의 부모
    p -> p = x; // p의 부모는 x
    if (b) b -> p = p; // b노드가 존재한다면 b의 부모는 p

    // x가 루트노드가 아닐경우
    // p노드가 p노드의 부모노드의 왼쪽이었다면 x를 왼쪽으로, 오른쪽이었다면 오른쪽으로
    (x -> p ? p == x -> p -> l ?  x -> p -> l : x -> p -> r : tree) = x;
    update(p), update(x);
}

 

roatate가 끝난 시점에서, p노드는 x노드의 자식노드이기 때문에

p노드에 대해 먼저 update후에 x노드를 update 해줘야 합니다.

 

이제 모든 노드의 서브트리의 크기를 cnt에 저장해 뒀으니 k-th element를 구할 수 있습니다.

 

void kth(int k){
    node* x = tree;
    while(1){
        while(x->l && x->l->cnt > k) x = x->l;
        if (x -> l) x -= x->l->cnt;
        if(!k--)break;
        x = x->r;
    }
    splay(x);
}

 

 

 

여기까지가 기본연산들에 대해 알아봤습니다.

이제 문제에 어떻게 적용할 수 있는지 알아보겠습니다.

대부분 배열에서의 쿼리 문제를 다룰 것입니다.

 

BBST이기 때문에 중위순회를 하면 항상 같은 순서로 탐색한다는 점을 이용합니다.

따라서 배열의 인덱스를 노드의 key로 생각해주면 배열의 원소를 스플레이트리로 관리할 수 있습니다.

 

간단한 구간합과 RMQ, 그리고 구간을 뒤집는 flip연산을 살펴보겠습니다.

 

splay연산을 통해 어떤 구간 [s, e]를 하나의 노드로 모으는 것에 집중해봅니다.

 

구간 [s, e]를 하나의 노드에 모으는 방법을 알아봅시다

s - 1번째 노드를 splay 해주면

s-1번째 노드가 루트가 되고 왼쪽 자식은 s - 2까지, 오른쪽 자식은 s부터의 구간에 대한 정보를 담고 있을것입니다.

 

그런 다음에 e + 1번 노드를 s - 1번 노드 오른쪽에 붙여주면

이렇게 될 것이고

루트의 오른쪽 자식의 왼쪽 자식에 [s, e] 구간에 대한 정보가 있을것입니다.

이것을 위해 splay 함수를 수정해서 루트로 올리는 함수에서 특정 노드를 다른 노드의 자식으로 가도록 수정합니다.

 

void splay(node* x, node* g = nullptr){
    node* y;
    while(x -> p != g){
        node* p = x -> p;
        if (p -> p == g){ // zig step
            rotate(x);
            break;
        }
        auto pp = p -> p;
        if ((p -> l == x) == (pp -> l == p)){ // zig-zig step
            rotate(p);
            rotate(x);
        }else{  // zig-zag step
            rotate(x);
            rotate(x);
        }
    }
    if(!g) tree = x;
}

 

구간 [s, e] 를 모으는 gather 함수는 다음과 같습니다.

node* gather(int s, int e){ // [s, e]구간을 관리하는 노드를 리턴
    kth(e + 1);
    auto tmp = tree;
    kth(s - 1);
    splay(tmp, tree);
    return tree->r->l;
}

 

 

이제 구간을 뒤집는 flip 함수를 보겠습니다.

구간을 뒤집는 연산은 왼쪽 서브트리와 오른쪽 서브트리를 swap하는 것을 재귀적으로 해주면 됩니다.

재귀적으로 처리하면 느리기 때문에 flip됐는지 여부를 lazy하게 처리해줍니다.

구조체에 변수를 추가합니다.

struct node{
	// 다른 변수들
    bool flip;
}

 

그리고 flip됐는지 여부를 전파할 함수를 만듭니다

 

void push(node *x){
    if (!x -> flip) return;
    swap(x->l, x->r);
    if (x -> l) x->l->flip = !x->l->flip;
    if (x->r) x->r->flip = !x->r->flip;
    x->flip = false;
}

 

flip함수는 다음과 같이 간단하게 구현이 가능합니다.

void flip(int s, int e){
    node* x = gather(s, e);
    x->flip = !x->flip;
}

 

 

실제 문제에 적용하면 또 코드가 구현하기 편하도록 수정이 됩니다.

이것은 문제 풀이 카테고리에 문제를 직접 풀며 올려보겠습니다.

 

 

 

 

전체 소스

struct node{
    node* l;
    node* r;
    node* p;
    int key, cnt;
    bool flip;
}*tree;

void update(node* x);

void rotate(node* x){ // 위로 올릴 노드
    node* p = x -> p; // x의 부모 노드
    node* b = NULL; // b노드
    if(!p) return; // x가 루트라면 리턴
    if (x == p->l){ // x가 p의 왼쪽 자식
        p -> l = b = x -> r; // p의 왼쪽을 b노드로
        x -> r = p; // x의 오른쪽 자식을 p노드로
    }else{ // x가 p의 오른쪽 자식
        p -> r = b = x -> l; // p의 오른쪽을 b노드로
        x -> l = p; // x의 왼쪽 자식을 p노드로
    }
    // 부모노드 재설정
    x -> p = p -> p; // x의 부모는 p의 부모
    p -> p = x; // p의 부모는 x
    if (b) b -> p = p; // b노드가 존재한다면 b의 부모는 p

    // x가 루트노드가 아닐경우
    // p노드가 p노드의 부모노드의 왼쪽이었다면 x를 왼쪽으로, 오른쪽이었다면 오른쪽으로
    (x -> p ? p == x -> p -> l ?  x -> p -> l : x -> p -> r : tree) = x;
    update(p), update(x);
}



void splay(node* x, node* g = nullptr){
    node* y;
    while(x -> p != g){
        node* p = x -> p;
        if (p -> p == g){
            rotate(x);
            break;
        }
        auto pp = p -> p;
        if ((p -> l == x) == (pp -> l == p)){
            rotate(p);
            rotate(x);
        }else{
            rotate(x);
            rotate(x);
        }
    }
    if(!g) tree = x;
}

void insert(int key){
    node* p =tree;
    node** pp; // 넣을 위치
    if(!p){ // 빈 트리인 경우
        node* x = new node;
        tree = x;
        x -> l = x -> r = x -> p = NULL;
        x -> key = key;
        return;
    }
    while(true){ // 삽입할 위치를 찾을때까지 반복
        if (key == p -> key) return; // 중복값이 있을 경우 종료
        if (key < p -> key){ // 현재 노드의 값보다 작을경우
            if (!p -> l){ // 왼쪽이 비어있으면 왼쪽에 삽입
                pp = &p->l;
                break;
            }
            p = p->l; // 비어있지 않으면 왼쪽 자식으로 이동
        }else{ // 현재 노드의 값보다 크다
            if (!p->r){
                pp = &p->r;
                break;
            }
            p = p->r;
        }
    }
    // 위에서 삽입할 위치를 찾음
    node* x = new node;
    *pp = x;
    x -> l = x -> r = NULL;
    x-> p = p;
    x-> key = key;
    // splay
    splay(x);
}

bool find(int key){
    node* p = tree;
    if(!p) return false; // 비어있는 트리
    while(p){
        if (key == p -> key) break; // 찾았다
        if (key < p -> key){
            if (!p -> l) break;
            p = p -> l;
        }else{
            if (!p -> r) break;
            p = p -> r;
        }
    }
    splay(p);
    return key == p->key;
}

void del(int key){
    if (!find(key)) return; // 삭제할 값이 없으면 루트로 이동 후 종료
    node* p = tree;
    if (p -> l && p -> r){ // 자식이 두개
        tree = p->l; // 왼쪽 자식이 새로운 루트
        tree->p = NULL;

        // 오른쪽 서브트리를 왼쪽 서브트리 아래에 삽입
        node* x = tree;
        while(x -> r) x = x -> r;
        x -> r = p -> r;
        p -> r -> p = x;
        delete p;
        return;
    }

    if(p->l){ //자식이 왼쪽만 있는 경우
        tree = p->l;
        tree->p = NULL; //왼쪽 자식이 새로운 루트
        delete p; //노드 삭제
        return;
    }
    if(p->r){ //자식이 오른쪽만 있는 경우
        tree = p->r;
        tree->p = NULL; //오른쪽 자식이 새로운 루트
        delete p; //노드 삭제
        return;
    }

    // 노드가 자신 하나인경우
    delete p;
    tree = NULL;
}

void update(node* x){
    x -> cnt = 1;
    if (x -> l) x->cnt += x->l->cnt;
    if (x -> r) x->cnt += x->r->cnt;
}

void kth(int k){
    node* x = tree;
    while(1){
        while(x->l && x->l->cnt > k) x = x->l;
        if (x -> l) x -= x->l->cnt;
        if(!k--)break;
        x = x->r;
    }
    splay(x);
}

node* gather(int s, int e){ // [s, e]구간을 관리하는 노드를 리턴
    kth(e + 1);
    auto tmp = tree;
    kth(s - 1);
    splay(tmp, tree);
    return tree->r->l;
}

void push(node *x){
    if (!x -> flip) return;
    swap(x->l, x->r);
    if (x -> l) x->l->flip = !x->l->flip;
    if (x->r) x->r->flip = !x->r->flip;
    x->flip = false;
}

void flip(int s, int e){
    node* x = gather(s, e);
    x->flip = !x->flip;
}

 

 


사진 출처 :

https://justicehui.github.io/hard-algorithm/2019/10/23/SplayTree4/

https://blog.chodaeho.com/posts/2021/splay-tree-1/

https://boj.ma/7346/t

 

7346번: 유전자 함수

 

boj.ma

 

주어지는 두 유전자 문자열의 길이를 같게 적절히 공백을 추가했을 때 얻어지는 결괏값의 최대치를 알고 싶습니다.

또한 같은 위치에 두 문자열 모두에 공백이 있으면 안 됩니다.

주어지는 문자열의 길이는 100을 넘지 않습니다.

 

완전탐색부터 생각해 보면

문자열 두 개를 만들건대 문자열 각각의 인덱스를 n, m이라고 하면 첫 번째 문자열의 n번째 위치에 공백을 넣거나, 두 번째 문자열의 m번째 위치에 공백을 넣거나, 두 문자열 모두에 공백을 넣지 않거나

3가지 경우에 대해 분기를 나누어 진행하면 답을 찾을 수 있을 것 같습니다.

하지만 모든 경우에 대해 해 본다면 시간초과입니다.

 

예제의 문자열을 그대로 들고 왔습니다.

AGTGATG
GTTAG

 

 

첫 번째 문자열의 3번 인덱스까지 만들었고,  두 번째 문자열의 2번 인덱스까지 만들었을 때 그 이후에 공백을 적절히 채워 최댓값을 찾는 것은 이전에 어떤 방식으로 공백을 채웠든 항상 일정할 것입니다.

따라서 dp로 최적화가 가능합니다.

 

코드(c++)

#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>

#define endl "\n"
#define NM 101010
#define MAX 1010100
#define BIAS 1048576
#define MOD 1000000007
#define X first
#define Y second
#define INF 100000000
#define FOR(i) for(int _=0;_<(i);_++)
#define pii pair<int, int>
#define pll pair<ll, ll>
#define all(v) v.begin(), v.end()
#define fastio ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
using namespace std;


int main() {
    fastio
#ifndef ONLINE_JUDGE
    freopen("input.txt", "r", stdin);
    freopen("output.txt", "w", stdout);
#endif
    vector<vector<int>> arr = {
            {5, -1, -2, -1, -3},
            {-1, 5, -3, -2, -4},
            {-2, -3, 5, -2, -2},
            {-1, -2, -2, 5, -1},
            {-3, -4, -2, -1, 0}
    };
    string s = "ACGT*";
    map<char, map<char, int>> mp;

    for(int i = 0; i < 5 ;i ++){
        for (int j = 0 ; j < 5; j++){
            mp[s[i]][s[j]] = arr[i][j];
        }
    }

    int t;
    cin >> t;
    while(t--){
        int n, m;
        string s1, s2;
        cin >> n >> s1 >> m >> s2;
        if (n < m) swap(n, m), swap(s1, s2);
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, -1));
        auto recur = [&](const auto&self, int a, int b) -> int{
            if (a == n){
                int total = 0;
                for (int i = b ;i < m; i++){
                    total += mp['*'][s2[i]];
                }
                return total;
            }
            if (b == m){
                int total = 0;
                for (int i = a ;i < n; i++){
                    total += mp[s1[i]]['*'];
                }
                return total;
            }
            int&ret = dp[a][b];
            if (ret != -1) return ret;
            ret = -INF;
            // 두개를 직접 비교
            ret = max(ret, self(self, a + 1, b + 1) + mp[s1[a]][s2[b]]);
            // s1을 공백으로 채운다
            ret = max(ret, self(self, a, b + 1) + mp['*'][s2[b]]);
            // s2를 공백으로 채운다
            ret = max(ret, self(self, a + 1, b) + mp[s1[a]]['*']);
            return ret;
        };

        cout << recur(recur, 0,0 ) << endl;

    }


    return 0;
}

 

약간 귀찮은 전처리만 해주면 됩니다

 

 

 

'알고리즘 문제 풀이' 카테고리의 다른 글

백준 2001 보석 줍기  (2) 2024.08.31
백준 17943 도미노 예측  (0) 2024.07.18
백준 23889 돌 굴러가유  (0) 2024.05.16
백준 31786 최고의 친구  (0) 2024.05.09
백준 31782 저체온증  (0) 2024.05.02

https://boj.ma/23889/t

 

23889번: 돌 굴러가유

 

boj.ma

 

돌이 굴러가는 위치들이 주어지고, 세울 수 있는 벽의 개수가 주어질 때 벽을 적절한 위치에 세워 파괴되는 모래성의 개수를 최소로 하고 싶습니다.

 

완전탐색부터 생각해 보면

주어지는 N개의 위치에 대해 M개의 벽을 놓는 모든 경우의 수에 대해 해 보는 것입니다.

$_{N} C_{M}$ 가지의 경우에 대해 돌을 놔보고, 그때마다 계산을 해주면? 당연히 시간초과입니다.

 

최적화를 생각해 봅시다

모든 위치에 벽을 놔보는 것이 오래 걸리니 벽을 놓지 않아도 되는 위치에 대해 생각해 봅시다.

 

저는 처음에 주어지는 돌이 굴러가기 시작하는 위치에만 벽을 놓으면 된다고 생각했습니다.

당연하게도 돌이 굴러가다가 막는 것보다 애초에 돌이 구르지 못하게 하는 것이 더 이득이기 때문입니다.

 

하지만 돌의 개수 K 또한 최대 N까지 가능하므로 돌의 위치에만 벽을 세워본다 하더라도 시간초과가 날 것입니다.

 

이제, 벽을 놓지 않아도 되는 돌의 위치에 대해 생각해 봅시다.

 

예를 들어 입력 예제가 다음과 같이

10 1 2
1 2 3 4 5 6 7 8 9 10
1 10

 

이런 형태라면

벽은 하나만 놓을 수 있고 돌이 굴러가기 시작하는 위치는 1, 10이므로

1번 위치에 벽을 세우면 10만큼의 모래성이 파괴되지만 10에 설치하게 되면 45만큼의 모래성이 파괴됩니다.

 

이것을 통해

각 돌마다 벽이 없을 때 파괴하는 모래성의 개수를 알고 있다면, 가장 많이 파괴하는 돌부터 막는 게 최적일 것이라고 생각했습니다.

 

따라서 돌마다 파괴하는 모래성의 개수를 전처리하여 구하고, 정렬하여 낮은 인덱스 순서대로 출력하면 됩니다.

 

코드 (c++)

#include <bits/stdc++.h>

#define endl "\n"
#define NM 100101
#define MAX 1010100
#define BIAS 1048576
#define MOD 1000000007
#define X first
#define Y second
#define INF 1000000000
#define FOR(i) for(int _=0;_<(i);_++)
#define pii pair<int, int>
#define pll pair<ll, ll>
#define all(v) v.begin(), v.end()
#define fastio ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//typedef __int128 i128;
using namespace std;


int main() {
    fastio
#ifndef ONLINE_JUDGE
    freopen("input.txt", "r", stdin);
    freopen("output.txt", "w", stdout);
#endif

    int n, m, k;
    cin >> n >> m >> k;
    vector<int> arr(n + 1), stone(k + 1), psum(n + 1, 0);
    for (int i = 1; i <= n; i++) cin >> arr[i];
    for (int i = 1 ;i <= k; i++) cin >> stone[i];
    for (int i = 1; i <= n; i++) psum[i] = psum[i - 1] + arr[i];
    vector<pii> v;
    for (int i = 1 ; i < k; i++){
        v.push_back({psum[stone[i + 1] - 1] - psum[stone[i] - 1], stone[i]});
    }
    v.push_back({psum[n] - psum[stone.back() - 1], stone.back()});

    auto comp = [&](pii a, pii b) -> bool{
        if (a.X == b.X) return a.Y < b.Y;
        return a.X > b.X;
    };

    sort(all(v), comp);

    vector<int> ans;

    int cnt = min(m, k);
    for (int i = 0 ; i < cnt; i++){
        ans.push_back(v[i].Y);
    }
    sort(all(ans));

    for (auto i : ans) cout << i << endl;

    return 0;
}

 

 

'알고리즘 문제 풀이' 카테고리의 다른 글

백준 17943 도미노 예측  (0) 2024.07.18
백준 7346 유전자 함수  (0) 2024.05.16
백준 31786 최고의 친구  (0) 2024.05.09
백준 31782 저체온증  (0) 2024.05.02
백준 31778 PPC 만들기  (1) 2024.05.02

https://boj.ma/31786/t 

 

31786번: 최고의 친구

 

boj.ma

 

2가지 쿼리가 주어집니다.

1번 쿼리 : $(x, 0), (-x, 0)$ 에 위치할때 통신 가능한 기지국의 개수 출력

2번 쿼리 : $(x, y)$ 에 기지국이 있었으면 삭제, 없었으면 추가

 

$(x, 0)$ 와 $(-x, 0)$ 이 통신하기 위해서는 어떤 기지국 $(a, b)$ 에 대해 세 점이 이루는 삼각형이 예각 삼각형이어야 합니다.

 

1번 쿼리를 보고 예각 삼각형이 되어야 하므로 당연히 점 $(a, b)$ 의 a가 $ -x < a < x $ 여야 한다는 건 알았습니다.

처음엔 저 사이에만 있으면 무조건 예각삼각형이라고 착각했습니다.

 

중3때 배우는 기하학을 갖고와봅시다

반원에 대한 원주각은 항상 직각입니다.

$(x, 0)$ 와 $(-x, 0)$ 을 지름으로 하는, 반지름의 길이가 x이고 중심이 $(0, 0)$인 원을 생각하면

원안에 있는 모든 점들에 대해서 만들어지는 삼각형은 절대 예각삼각형이 될 수 없습니다.

 

따라서 임의의 기지국 좌표 $(a, b)$에 대해 $ -x < a < x $ 를 만족하면서  $ a^2 + b^2 > x^2$ 인 점들만 답이 될 수 있습니다.

그럼  $ -x < a < x $ 의 개수에서 $ a^2 + b^2 <= x^2$ 인 점의 개수를 빼주면 1번 쿼리를 처리할 수 있습니다.

 

쿼리의 개수가 $3*10^5$개 주어지는 좌표의 범위가 $[-10^9, 10^9]$ 이므로 당연히 완전탐색으로 쿼리를 처리할 수 없습니다.

 

1번쿼리는 특정 범위인 값의 개수, 2번쿼리는 특정 좌표를 추가하거나 삭제하는 쿼리이므로

세그먼트 트리를 생각할 수 있습니다.

그런데, 좌표의 범위가 크기때문에 좌표압축을 해줘야할 것 같은데 온라인으로 좌표가 주어지기때문에 힘들어 보입니다.

 

그래서 쿼리를 미리 다 받아서 나올 수 있는 모든 $x$좌표와 $x^2+y^2$의 값을 미리 계산하여 좌표압축 후에

세그먼트 트리를 적용하면 됩니다.

 

여기까지 생각하고 세그먼트 트리를 통해 구현을 하다가 pbds를 통해서도 충분히 위의 쿼리를 처리할 수 있다고 판단하여 pbds로 구현했습니다.

 

그리고, $x^2 + y^2$의 값들을 관리할때 좌표가 다르더라도 $x^2 + y^2$의 결과는 같을 수 있습니다.

따라서 기지국의 좌표를 따로 관리하는 자료구조(set, map..)도 필요합니다.

 

구현(c++)

#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>

#define endl "\n"
#define NM 101010
#define MAX 1010100
#define BIAS 1048576
#define MOD 1000000007
#define X first
#define Y second
#define INF 100000000
#define FOR(i) for(int _=0;_<(i);_++)
#define pii pair<int, int>
#define pll pair<ll, ll>
#define all(v) v.begin(), v.end()
#define fastio ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//typedef __int128 i128;
using namespace std;

//pbds -- order_of_key(x) := x미만의 원소의 개수(x가 들어가야할 인덱스 0-based) , find_by_order(k) := (k + 1)번째 원소가 있는 iter
using namespace __gnu_pbds;
// multi-set
using ordered_set_equal = tree<ll, null_type, less_equal<ll>, rb_tree_tag, tree_order_statistics_node_update>;
using ordered_set_equal_pll = tree<pll, null_type, less_equal<pll>, rb_tree_tag, tree_order_statistics_node_update>;
// set
using ordered_set = tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update>;


int main() {
    fastio
#ifndef ONLINE_JUDGE
    freopen("input.txt", "r", stdin);
    freopen("output.txt", "w", stdout);
#endif
    ordered_set_equal x_, xy_;
    ordered_set_equal_pll xy_cd;
    // multi-set 원소삭제
    auto pbds_erase = [&](ordered_set_equal &a, ll val)->void{
        auto it = a.find_by_order(a.order_of_key(val));
        if(*it == val) a.erase(it);
    };

    auto pbds_erase_pll = [&](ordered_set_equal_pll &a, pll val) -> void{
        auto it = a.find_by_order(a.order_of_key(val));
        if (*it == val) a.erase(it);
    };

    int q;
    cin >> q;
    while(q--){
        int oper;
        cin >> oper;
        if (oper&1){
            ll x;
            cin >> x;
            // (-x, x)구간의 갯수
            int x_cnt = x_.order_of_key(x) - x_.order_of_key(-x + 1);
            // a^2 + b^2 <= x^2인 (a, b)의 갯수
            int xy_cnt = xy_.order_of_key(x * x + 1);
            cout << x_cnt - xy_cnt<< endl;
        }else{
            ll x, y;
            cin >> x >> y;
            // x^2 + y^2
            ll cur = x * x + y * y;
            auto it = xy_cd.find_by_order(xy_cd.order_of_key({x, y}));
            // 이미 존재하는 기지국
            if (it->first == x && it->second == y) {
                pbds_erase(xy_, cur);
                pbds_erase(x_, x);
                pbds_erase_pll(xy_cd, make_pair(x, y));
            }else{
                xy_.insert(cur);
                x_.insert(x);
                xy_cd.insert(make_pair(x, y));
            }
        }
    }

    return 0;
}

 

 

 

기지국의 좌표를 관리하는건 굳이 pbds를 통해 관리하지 않아도 됩니다.

 

 

 

 

사진 출처 : http://trsketch.dothome.co.kr/a13012

'알고리즘 문제 풀이' 카테고리의 다른 글

백준 7346 유전자 함수  (0) 2024.05.16
백준 23889 돌 굴러가유  (0) 2024.05.16
백준 31782 저체온증  (0) 2024.05.02
백준 31778 PPC 만들기  (1) 2024.05.02
백준 10719 Baekjoon Database Management System  (0) 2024.04.30

https://boj.ma/31782/t 

 

31782번: 저체온증

 

boj.ma

 

문제 조건에 따라 낮동안에 정상체온이 되는 사람들의 그룹은 반드시 직사각형 형태가 됩니다.

 

밤에는 K명이 저체온증이 되는데 만들어지는 직사각형들 중 가로 또는 세로의 길이가 K보다 작거나 같다면 그 변에 있는 모든 사람이 저체온증에 걸리게 하면 낮동안에 정상체온이 될 방법이 없으므로 그 직사각형 그룹은 모두 저체온증이 됩니다.

 

따라서 bfs 한 번으로 정상체온 그룹을 만들어주고,

 

그룹을 만날때마다 가로, 세로 사이즈를 구해서 모두 K보다 크다면 직사각형의 크기만큼을  정답에 추가해주면 됩니다.

총 시간복잡도는 O(NM)입니다.

 

코드(python)

import sys
from collections import deque
input = sys.stdin.readline

def in_range(a, b):
    return 0 <= a < n and 0 <= b < m

n, m, k = map(int, input().split())
board = [list(input().strip()) for _ in range(n)]
q = deque()
visited = [[False] * m for _ in range(n)]
dxy = [(1, 0), (0, 1), (-1, 0), (0, -1)]
for i in range(n):
    for j in range(m):
        if board[i][j] == 'O':
            q.append((i, j))
            visited[i][j] = True

while q:
    x, y = q.popleft()
    board[x][y] = 'O'
    for dx, dy in dxy:
        nx, ny = x + dx, y + dy
        if not in_range(nx, ny):
            continue
        if visited[nx][ny]:
            continue
        if board[nx][ny] == '.':
            cnt = 0
            for ddx, ddy in dxy:
                nnx, nny = nx + ddx, ny + ddy
                if not in_range(nnx, nny):
                    continue
                if board[nnx][nny] == 'O':
                    cnt += 1
            if cnt >= 2:
                q.append((nx, ny))
                visited[nx][ny] = True
visited = [[False] * m for _ in range(n)]
ans = 0
for i in range(n):
    for j in range(m):
        if board[i][j] == 'O' and not visited[i][j]:
            garo = 0
            saero = 0
            x = i
            y = j
            while x < n and board[x][j] == 'O':
                x += 1
                saero += 1
            while y < m and board[i][y] == 'O':
                y += 1
                garo += 1
            q = deque()
            q.append((i, j))
            visited[i][j] = True
            while q:
                x, y = q.popleft()
                for dx, dy in dxy:
                    nx, ny = x + dx, y + dy
                    if not in_range(nx, ny):
                        continue
                    if visited[nx][ny]:
                        continue
                    if board[nx][ny] != 'O':
                        continue
                    q.append((nx, ny))
                    visited[nx][ny] = True
            if garo > k and saero > k:
                ans += garo * saero
print(ans)

 

 

가로세로 체크하며 이미 방문한 그룹을 다시 방문하지 않기 위해 BFS를 한 번 더 했는데 굳이 이렇게 하지 않고 더 효율적으로 하는 방법이 있습니다. 저는 비효율적으로 구현한것 같습니다.

 

+ Recent posts