Lock Free 알고리즘(Non-Blocking 알고리즘)

마지막 업데이트: 2022년 2월 6일 | 0개 댓글
  • 네이버 블로그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 트위터 공유하기
  • 카카오스토리 공유하기
이 글에서는 네트워크 구조에 대한 gradient 를 사용하는 경우만 gradient-based method 라고 부른다. 즉, RL 기반 방법론에서 사용하는 gradient 는 controller parameter 에 대한 gradient 이기 때문에 여기서는 gradient-based method 라 하면 RL 은 포함되지 않는다.

Overview

본 글은 총 5개의 포스트로 구성되며, 첫번째 포스트에서는 AutoML 과 Neural Architecture Search (NAS) 의 여러 알고리즘들을 다룬다. 그리고 나머지 4개의 포스트에서는 개인적으로 인상깊게 읽은 논문인 DARTS: Differentiable Architecture Search 에 대해서 이론과 코드를 자세히 설명하고, 이를 multi-gpu 까지 확장한다.

AutoML

AutoML 은 Automated Machine Learning 의 약자로, 말 그대로 머신러닝의 자동화에 대한 분야다. 머신러닝으로 문제를 푼다고 할 때 그 과정을 자동화하는 것을 목표로 하며, 하이퍼파라메터 서치가 대표적인 케이스다. 딥러닝이 대세로 자리잡은 이후로 단순히 하이퍼파라메터를 최적화하는 것이 아니라 네트워크 구조를 자동으로 찾아주는 방법에 대한 필요성이 커졌으며, 이것이 바로 최근에 주목받고 있는 Neural Architecture Search (NAS) 다.

이 글에서는 NAS 의 주요 논문들에 대해 간단하게 살펴보고, 개인적으로 가장 흥미롭게 읽은 논문인 DARTS 에 대해 자세히 살펴본다.

Problem setting

NAS 를 포함하여 AutoML 은 최적화 문제다. 어떤 네트워크 구조를 사용할 지, 어떤 하이퍼파라메터를 사용할 지 등을 search space 로 갖고, 거기서 validation loss 혹은 accuracy 등을 objective function 으로 갖는다. 그럼 이제 이 서치 스페이스를 잘 탐색하여 objective function 을 잘 최적화하면 되는 문제다.

그렇다면 이 문제 정의로부터 연구의 방향도 생각해 볼 수 있다. 1) 서치 스페이스를 어떻게 정의하느냐 2) 어떤 최적화 알고리즘으로 이 서치 스페이스를 탐색하느냐 3) objective function 을 어떻게 정의하느냐 4) objective function 을 어떻게 계산하느냐. 여기서 4번이 좀 생소하게 느껴질 수 있는데, 이는 objective function 의 값을 계산하기가 어려운 AutoML 의 문제적 특성 때문에 발생한다. objective function 을 validation accuracy 로 정의했다면, 이 값을 계산하기 위해서는 네트워크를 통째로 학습하여 그 결과를 확인해야 하므로 엄청난 계산량이 요구된다. 따라서 AutoML 에서는 이 objective function 을 어떻게 빨리 계산할 수 있을지도 주요한 챌린지가 되는 것이다.

Categorized by optimization methods

NAS 논문들을 다양한 관점에서 분류할 수 있겠지만, 여기서는 어떤 최적화 방법을 사용했는지에 따라 나누어 설명한다. 기본적으로 NAS 에서 주로 사용하는 objective function 인 validation accuracy 가 non-differentiable objective function 이기 때문에 objective function 의 gradient 를 직접적으로 사용하지 않는 방법들이 주를 이룬다. 이 글에서는 각 최적화 방법론에 대한 설명은 따로 하지 않으며 어느정도 알고 있다고 가정한다.

이 글에서는 네트워크 구조에 대한 gradient 를 사용하는 경우만 gradient-based method 라고 부른다. 즉, RL 기반 방법론에서 사용하는 gradient 는 controller parameter 에 대한 gradient 이기 때문에 여기서는 gradient-based method 라 하면 RL 은 포함되지 않는다.

참고로, 아래 논문들의 약어는 대부분 논문에서 제안한 약어이나 NASRL 과 TNAS 는 따로 제안한 약어가 없어 임의로 정하였다. AmeobaNet 과 AutoKeras 는 각 논문의 산출물의 이름인데 이를 약어로 사용한다.

또한 대부분의 논문들이 CNN 과 함께 RNN 에 적용한 결과도 보이나, 본 글에서는 CNN 구조를 찾는 방법에 집중한다. RNN 도 디테일한 부분이 달라질 뿐 큰 부분에서는 동일하다.

      . Zoph, Barret, and Quoc V. Le. “Neural architecture search with reinforcement learning.” arXiv preprint arXiv:1611.01578 (2016). . Zoph, Barret, et al. “Learning transferable architectures for scalable image recognition.” arXiv preprint arXiv:1707.07012 2.6 (2017). . Pham, Hieu, et al. “Efficient Neural Architecture Search via Parameter Sharing.” arXiv preprint arXiv:1802.03268 (2018). . Tan, Mingxing, et al. “Mnasnet: Platform-aware neural architecture search for mobile.” arXiv preprint arXiv:1807.11626(2018).
      . Real, Esteban, et al. “Regularized evolution for image classifier architecture search.” arXiv preprint arXiv:1802.01548 (2018).
      . Jin, Haifeng, Qingquan Song, and Xia Hu. “Efficient neural architecture search with network morphism.” arXiv preprint arXiv:1806.10282 (2018).
      . Liu, Chenxi, et al. “Progressive neural architecture search.” arXiv preprint Lock Free 알고리즘(Non-Blocking 알고리즘) arXiv:1712.00559 (2017).
      . Liu, Hanxiao, Karen Simonyan, and Yiming Yang. “Darts: Differentiable architecture search.” arXiv preprint arXiv:1806.09055 (2018).

    Reinforcement Learning

    RL 기반 방법론들은 validation accuracy 를 reward 로 보고, 네트워크의 각 파트들을 action 으로 본다. 여러번의 action 을 통해 네트워크를 정의하고 나면 학습을 통해 reward 를 얻을 수 있으며, 이 값으로 action 을 생성하는 policy 를 학습한다. State 는 각 시점까지 완성된 네트워크의 구조라고 할 수 있으나, 이를 명시적으로 사용하지 않고 policy 를 RNN 으로 디자인함으로써 implicit 하게 가져간다. 이러한 policy RNN 을 controller 라고 하며, 컨트롤러가 생성한 네트워크의 구조를 child network 라 부른다.

    NASRL

    NAS 를 널리 알린 첫 논문. 첫 논문인 만큼 서치 스페이스 디자인이나 셀 디자인 등 전체적인 디자인이 꽤나 둔탁하다.

    이러한 RNN controller 가 레이어 각각의 convolution 연산의 특성과, skip-connection 을 어떤 레이어로 연결할지 결정한다. skip-connection 은 어텐션 구조를 활용하여 결정하는데, softmax 가 아니라 sigmoid 로 결정하기 때문에 여러 레이어에 skip-connection 이 가능하다. 참고로 레이어 수는 가변이 아니며 하이퍼파라메터로 정해준다.

    그 결과로 얻어지는 게 위와 같은 괴랄한 구조인데, 흥미로운 점은 이게 적어도 local optimum 이라는 것이다. 논문에서는 이 구조에 skip connection 을 더하거나 빼서 실험을 해 보았으나 결과가 나빠졌다고 보고하고 있다.

    전체 네트워크를 다 디자인하는 것이 아니라, 셀 구조를 찾아서 스택하는 방식을 적용하였다. 또한 작은 데이터셋 (CIFAR-10) 에서 셀 구조를 찾고, 이 셀을 더 스택하여 큰 데이터셋 (ImageNet) 에 적용하는 방식을 제안. 이러한 방식을 통해 ImageNet 에서 SOTA 를 찍었다.

    위와 같이 CIFAR10 에 대해 Normal cell 과 featuremap size 를 줄이는 Reduction cell 을 각각 찾은 뒤 그대로 ImageNet 에 적용한다. ImageNet 에 적용할 때에는 셀을 스택하는 횟수 N 과 Lock Free 알고리즘(Non-Blocking 알고리즘) 채널 사이즈 C 는 재조정한다.

    이 논문은 NAS 에 필요한 디테일한 부분들을 많이 정립했다. 셀 디자인과 서치 스페이스를 새로이 정의하며, 여기서 정의한 구조와 다양한 테크닉들은 이후 논문들에서도 계속 사용된다.

    NASRL 과는 다르게 skip-connection 을 2개로 고정하며, operation 또한 이전처럼 filter size 와 stride size 를 찾는 것이 아니라 자주 쓰이는 operation 들을 후보로 놓고 그 안에서 탐색한다:

    이렇게 2개의 input 을 받아서 각각 연산을 한 후 combine 연산을 통해 합쳐지는 것을 하나의 블럭이라고 하는데, 이러한 블럭들이 모여 하나의 셀을 이룬다. 이러한 형태의 블럭 구조를 사용함으로써 시퀀셜한 구조였던 NASRL 과 달리 네트워크 구조에 topology 를 가할 수 있다.

    위 그림은 이러한 방식으로 찾은 Lock Free 알고리즘(Non-Blocking 알고리즘) NASNet-A 의 셀 구조이며, 이렇게 두 종류의 셀을 찾으면 이를 스택하여 큰 데이터셋에 적용할 수 있다.

    이렇게 찾은 NASNet 구조는 ImageNet 에서 SOTA 를 찍었으며, 이후로도 이 이상의 큰 성능향상은 찾아보기 어렵다.

    NASRL 과 TNAS 를 포함하여 ENAS 이전까지의 NAS 는 대부분 어마어마한 양의 컴퓨테이션 리소스가 소모되었다. gpu days 라는 단위를 사용하는데, 1개의 gpu 를 사용한다고 했을 때 며칠이 걸리는지를 나타내는 단위다. 이게 ENAS 이전의 논문들은 수백 혹은 수천에 이른다 (심지어 NASRL은 만 단위다) - 따라서 gpu 하나만 가지로 학습한다고 하면 최소한 3년 이상이 걸린다는 것이다! ENAS 에서는 이를 줄이기 위한 여러가지 효율적인 테크닉들을 제안하고, 이를 통해 이 수천에 이르던 필요 리소스를 0.5 gpu day 로 줄여 버린다.

    이를 위해 parameter sharing 이라는 아이디어를 제안하는데, 그 방법이 꽤나 재미있다. 위치와 인풋과 연산이 동일하다면 서로 parameter 를 공유한다. 즉 동일한 위치에서 동일한 인풋을 받고 동일한 연산을 한다면, 네트워크의 다른 부분이 어떻게 생겨먹었든 동일한 파라메터를 사용한다!

    예를 들어, 위와 같은 directed acyclic graph (DAG) 구조의 search space representation 을 사용한다고 해 보자. 위 그림은 RNN cell architecture search 그림이다. 각 노드는 activation function 을 의미하고, 각 엣지는 input node 를 의미한다. 즉 2번 노드는 ReLU activation 을 사용하고, 1번 노드의 아웃풋을 인풋으로 받는다. RNN cell 이기 때문에 연산은 무조건 fully-connected 다. 따라서 만약 동일한 노드에서 동일한 인풋 엣지를 갖는다면 연산은 동일하므로 파라메터가 공유된다. 위 그림에서 3번 노드는 ReLU 를 사용하며 2번 노드를 input node 로 갖는데, 이 부분만 동일하다면 다른 부분이 싹 달라져도 파라메터를 공유한다는 것이다.

    위 DAG 구조는 단순히 예를 위해서 든 것이 아니라 실제로 ENAS 에서 새로이 제안한 search space representation 이다. 위 설명에서 언급하지 않았지만 NASRL 은 RNN cell 을 찾을 때 LSTM 의 구조를 차용한 tree topology 를 사용한다. ENAS 에서는 이러한 트리 구조를 DAG 로 변경함으로써 더욱 유연한 구조를 제안한다. DAG 는 RNN cell 뿐만이 아니라 CNN 에도 적용 될 수 있는데, DAG 구조를 사용하여 CNN 전체 구조를 찾는 것을 macro search 라 하고, TNAS 에서 제안한 방식을 micro search 라 한다. 논문에서는 두 방식을 비교하는데 micro search 의 성능이 더 좋다.

    이 외에도 최대한 효율적으로 탐색을 하기 위해 몇 가지 추가적인 아이디어가 도입된다. NASRL/TNAS 에서는 RNN controller 가 mini-batch 단위로 네트워크 구조를 생성하고, 그 구조들을 전부 학습한 뒤에 업데이트한다. ENAS 에서는 RNN controller 가 생성한 네트워크 구조들을 1-step 씩만 학습시켜 성능을 비교하고 제일 좋은 구조 하나만 학습한다. 또한 search space 도 극단적으로 줄였는데, 심지어 3x3 convolution 조차 search space 에 없다 (micro search 의 경우).

    MNAS 는 지금까지의 방법들과 목표가 다르다. 모바일에 적용가능한 NAS 를 찾는 것이 목표. 지금까지 살펴본 (그리고 앞으로 살펴볼) NAS 들은 전부 validation accuracy 를 objective function 으로 두고 이를 최적화하는 것을 목표로 한다. 그러나 모바일 세팅에서는 네트워크의 정확도를 조금 희생하더라도 가벼운 네트워크를 만드는 것이 매우 중요하다. 이렇게 모바일 환경에서 잘 작동하는 가벼운 네트워크를 찾는 것은 image classification 분야 안에서 새로운 하나의 분야를 이루고 있으며, 대표적으로는 MobileNet, ShuffleNet 시리즈들이 있다.

    목표가 그렇다면 적용은 Lock Free 알고리즘(Non-Blocking 알고리즘) 어렵지 않다. Reward 에 validation accuracy 뿐만이 아니라 latency (inference speed) 를 추가하여 최적화하면 된다. MNAS 이전에도 이러한 논문들이 있었으나, MNAS 에서는 latency constraint 를 soft 하게 걸어줌으로써 성능을 높이고 여러 latency 에 대한 Pareto optimal 을 찾을 수 있게 하였다.

    이전 연구들에서는 latency 에 대한 제한을 $LAT(m) \le T$ 와 같이 항상 T 보다 작거나 같도록 hard constraint 를 걸어주었지만, MNAS 에서는 위와 같이 soft constraint 로 변형하였다.

    그 결과 위와 같은 Pareto optimal 을 얻을 수 있다. 가로축이 latency 로, 한번의 탐색으로 60 ~ 110 ms 의 latency 에 대해 각각 최적화된 구조를 찾을 수 있다.

    뿐만 아니라 MNAS 에서는 search space 도 새롭게 제안하는데, TNAS 에서 제안한 셀 구조를 찾아서 동일한 셀을 스택하는 방식을 따르는 것이 아니라, 모든 셀의 구조를 다르게 가져간다. 다만, 각 셀은 연산 하나로 정의되며 동일한 연산을 여러개 스택하여 하나의 셀을 이룬다. 그림에서 Block 2 는 3x3 conv 연산을 사용하는데, 그러면 Block 2 의 모든 레이어는 Lock Free 알고리즘(Non-Blocking 알고리즘) 3x3 conv 가 되는 것이다. 조금 아쉬운 것은 이렇게 새로이 제안한 search space 에 대한 justification 이 없는 것인데, 논문에서 TNAS 의 셀 스택 구조와 비교 실험은 수행하지 않았다.

    이 논문의 재미있는 insight 중 하나는 5x5 depthwise separable conv 에 있다. MNAS 에서 최종적으로 찾아낸 구조는 5x5 depthwise separable conv 를 사용하는데, 일반적으로 5x5 conv 는 3x3 conv 2개로 대체될 수 있기 때문에 잘 사용하지 않는다. 그러나 depthwise separable conv 에서는 5x5 하나가 3x3 2개보다 적은 연산량을 가진다.

    Evolutionary Algorithms

    사실 “네트워크 구조를 자동으로 찾아주는 알고리즘” 이라고 하면 바로 처음 떠오르는 방법이 아마도 EA 일 것이다. EA 는 있는 그대로 NAS 에 들어맞는다. 때문에 EA 로 NAS 를 수행하는 연구는 NeuroEvolution 이라고 불리며 오래된 역사를 갖고 있다.

    위 알고리즘은 AmeobaNet 논문의 알고리즘이다. EA 를 알고 있다면, 위 알고리즘을 찬찬히 보면 대충 어떤 방식인지 알 수 있다 (사실 별 게 없다). Population 에서 랜덤 샘플링을 해서 좋은 모델은 mutation 을 거쳐 새로이 population 에 추가되고, 안 좋은 모델은 제거된다.

    AmeobaNet

    위 설명과 알고리즘이 안 맞는 부분이 있다. 샘플된 모델들 중에서 안 좋은 모델이 제거된다고 했는데, 위 알고리즘에는 Oldest 라고 되어 있다. 이 논문에서는 이와 같이 worst 를 제거하는 것이 아니라 oldest 를 제거하는 regularized evolution (RE) 을 제안하였다.

    그리고 그게 끝이다. 뭐 없다고 느낀다면 그게 맞다. 그래서인지, 아니면 처음부터 의도가 그랬는지는 모르겠지만, 이 논문의 메인 컨트리뷰션은 새로운 방법의 제안이 아니라 EA 가 RL 보다 좋다는 것을 실험적으로 보였다는 데에 있다.

    위 그래프에서 볼 수 있다시피 EA 가 RL 보다 더 빨리 수렴한다. 다만 최종 성능은 비슷하다고 한다. 애매한 컨트리뷰션 때문에 accept 이 잘 안된 것인지, 아카이브에 현재 버전 6으로 초기 버전에 비하면 상당히 많이 수정되었다.

    Bayesian optimization

    그런데 이 쯤에서 생각해 볼 것이 하나 있다. 왜 최적화 문제를 푸는데 우리에게 가장 친숙한 gradient descent 를 사용하지 않고 엄한 RL 이니 EA 니 들먹이고 있는가? 그건 우리의 objective function 이 non-differentiable 하기 때문이다. 그 때문에 최적화를 위해 gradient-based method 를 사용할 수 없고, non-differentiable objective 에 적용가능한 알고리즘인 RL 과 EA 가 나온 것이다. 그렇다면 이 맥락에서 빠질 수 없는 것이 Bayesian optimization (BO) 다.

    AutoKeras

    이 BO 를 사용한 논문이 바로 AutoKeras 가 나온 “Efficient neural architecture search with network morphism” 이다. 먼저 논문의 이름을 잘 보면 network morphism 이라는 단어가 나오는데, 이건 현재 네트워크의 지식을 유지하면서 네트워크의 구조를 바꾸는 방법이다. 즉, 학습된 네트워크의 구조를 바꾸면서도 동일한 input 에 대해 동일한 output 이 나오도록 유지한다는 것이다. 이는 위에서 언급한 4가지 연구 방향 중 4번에 해당하는데, objective function 을 계산하기 위한 비용이 너무 크기 때문에 학습된 네트워크의 지식을 유지함으로써 보다 빠르게 계산하기 위해서 사용된다. 즉 새로운 네트워크 아키텍처를 생성했을 때, 원래 이 네트워크 구조의 objective function 을 계산하려면 학습 후 validation accuracy 를 계산해야 하지만, network morphism 을 통하면 이미 상당 수준의 학습이 된 녀석이 나오기 때문에 약간만 추가 학습하여 objective function 을 계산할 수 있다. 이러한 network morphism 방식은 여기서만 사용되는 것이 아니며 objective function 을 빠르게 계산하기 위해 NAS에서 종종 사용된다.

    자, 그럼 이제 BO 를 하면 된다! 그런데 이를 위해 필요한 것이 몇 가지 있다. 가장 먼저 kernel function 을 정의해야 한다. NAS 의 search space 가 Euclidean space 가 아니기 때문에 이에 맞는 커널 펑션을 새로이 정의해야 한다. 이 논문에서는 이 커널 펑션을 두 네트워크의 구조 간 edit distance 로 정의한다. 그 다음이 acquisition function 인데, 여기서는 UCB 를 사용하면서 새로이 정의한 search space 에 맞는 학습 방법을 제안한다.

    이 논문은 AutoKeras 로 꽤나 화제가 되었지만 논문의 결과는 사실 그렇게 인상적이지 않다. 위 수치 자체도 그다지 좋지 않으며, 비교 대상으로 선정한 방법들도 SOTA 들이 아니다. 브랜딩을 잘 한 논문으로 보인다.

    Sequential Model-Based Optimization

    Bayesian optimization 의 덩치가 크기 때문에 굳이 나누어 놓았지만, 사실 BO 는 Sequential Model-Based Optimization (SMBO) 의 일종이다. Model-based optimization 이란 최적화를 할 때 실제 값을 관측하는 비용을 줄이고자 실제 값을 예측하는 모델을 학습하는 방법이다. SMBO 는 여기서 모델을 학습하고, 학습한 모델에 기반하여 새로운 데이터를 얻는 과정을 iterative 하게 반복한다.

    이를 NAS 에 적용해보자. 모델의 구조를 보고 성능을 예측하는 surrogate model 이 있다고 하면,

    1. 모든 가능한 구조들에 대해 성능을 예측해보고, 결과가 좋을 것 같은 구조를 고른다.
    2. 고른 구조들로 실제로 학습해서 결과를 본다.
    3. 새로 생긴 데이터들로 surrogate model 을 추가 학습한다.

    이 과정을 반복하면 된다. 여기서 surrogate model 로 Gaussian process (GP) model 을 사용하면 Bayesian optimization 이 되는 것이다.

    PNAS 는 Progressive NAS 로, 블럭 크기를 작게 시작하여 점차 키워나가는 방식을 사용한다. Search space representation 은 TNAS 의 것을 가져오며, 다만 TNAS 의 서치 과정에서 사용되지 않은 연산들은 일부 제거한다. 또한 normal cell 과 피처맵 사이즈를 줄이는 reduction cell 을 따로 찾았던 TNAS 와는 달리, PNAS 에서는 normal cell 하나만 찾고 stride 를 조정하여 reduction cell 로 사용한다.

    위와 같이 TNAS 와 동일하게 블럭 5개를 모아서 셀을 만들고, 셀을 스택하여 네트워크를 만든다. 작은 데이터셋에서 셀을 찾아서 큰 데이터셋에 적용하는 것도 동일하다. PNAS 는 여기에 더해 학습 속도를 빠르게 하기 위해 progressive 한 방식을 도입한다. RNN controller 를 사용하고 이를 RL 로 학습했던 TNAS 와는 달리, 작은 셀 구조에서 시작하여 모든 구조의 성능을 측정하면서 좋은 놈들만 남겨가며 점차 셀의 크기를 키우는 방식을 사용한다.

    복잡해 보이지만 간단하다. 실제로 모든 구조를 다 학습시켜서 성능을 측정하는 것은 너무 큰 비용이 들어가므로, 네트워크 구조를 보고 학습 후의 성능을 예측하는 surrogate function 을 사용한다.

    1. b=1 (블럭 개수 1개) 에 대해 모든 구조를 다 만들어서 학습 후 surrogate function 학습
    2. b=2 로 블럭 개수를 증가시켜서 모든 구조를 다 생성한 후 surrogate function 을 통해 top-k 필터링
    3. 고른 구조들을 학습시켜서 실제 성능을 측정하고 이 데이터로 다시 surrogate function 학습
    4. b가 원하는 크기가 될 때까지 2~3 반복.

    PNAS 는 이러한 방식으로 수천에 달했던 gpu days 를 수백까지 줄이는데 성공한다. 물론 위에서 언급한 ENAS 나 앞으로 설명할 DARTS 보다는 훨씬 큰 수치지만, 글 작성 순서 상 뒤에 있을 뿐 PNAS 가 먼저 나온 논문이라는 점을 잊지 말자.

    Gradient descent

    지금까지 계속 non-differentiable objective function 을 최적화하기 위해 gradient 를 사용하지 않는 방법들을 적용했다. 하지만 그게 아니라 objective function 을 변형해서 differentiable 하게 바꿀 수도 있지 않을까? DARTS 가 바로 그런 논문이다! 가장 인상적으로 읽은 논문이었기에 앞으로 3개의 포스트에 걸쳐 자세히 소개한다.

    Related works

    AutoML 에 대한 한국어 리뷰들은 아래에서도 찾아볼 수 있다. 여기서는 Lock Free 알고리즘(Non-Blocking 알고리즘) 간단히 논문의 컨셉만 소개하였으니 좀 더 자세한 내용이 궁금하다면 논문과 아래 페이지들을 참조하자. 특히 OpenResearch 에서는 여기서 다루지 않는 논문들도 다루고 있다.

    성공 알고리즘 EA

    병렬 알고리즘과 관련해서 최근의 연구 결과를 보면 대부분이 Non-Blocking 알고리즘, 즉 여러 스레드가 동작하는 환경에서 데이터의 안정성을 보장하는 방법으로 락을 사용하는 대신 저수준의 하드웨어에서 제공하는 compare-and-swap 혹은 compare-and-set (이후 CAS연산이라고 하겠다.)등의 명령을 사용하는 알고리즘을 다루고 있다. Non-Blocking 알고리즘은 운영체제나 JVM에서 프로세스나 스레드를 스케줄링 하거나 가비지 컬렉션 작업, 그리고 락이나 기타 병렬 자료 구조를 구현하는 부분에서 많이 사용되고 있다.

    다음과 같은 순서로 글을 써내려가겠다.

    락 기반 알고리즘의 단점

    공유된 자원에 대하여 여러 스레드에서 읽고 쓰기를 해야 한다면 개발자들은 보통 Mutex나 Semaphore를 이용하여 Lock을 걸고 Lock을 획득한 스레드를 제외한 다른 스레드들은 해당 자원에 접근하지 못하도록 구현할 것이다. JVM은 기본적으로 Monitor라는 개념이 사용되며 이는 Mutex와 동일한 역할을 한다. 고성능이 필요로 하지않는 상황에서는 굉장히 편한 방법이지만 고성능을 요구하는 어플리케이션에서 Lock기반으로 임계 영역을 관리한다면 요구사항을 충족 시키지 못할 것이다. Lock을 확보하지 못한 스레드는 실행되지 못하고 Lock을 획득한 스레드가 Lock을 Release할 때까지 대기 상태에 머물러야 하며 조건이 충족 될때 다시 실행시켜야한다. Lock을 획득하더라도 실제 CPU를 할당 받기전에 이미 CPU를 사용하고 있는 다른 스레드가 CPU할당량을 모두 사용하고 CPU 스케줄을 넘겨줄 때까지 대기해야한다. Lock에 대한 경쟁이 심해질수록 실제로 필요한 작업을 처리하는 시간 대비 동기화 작업에 필요한 시간의 비율이 높아지면서 성능이 떨어지게된다. 극단적인 경우에는 Lock을 획득한 스레드가 스케줄 대상에서 우선 순위가 낮아져 Lock을 Release하는 시간이 늦어진다면 공유 자원을 사용하기 위해 기다리는 다른 스레드들도 모두 대기 상태에 머무르게 되는 상황이 발생할 수 있다.

    병렬 연산을 위한 하드웨어 지원

    세밀하고 단순한 작업을 처리하는 경우에는 일반적으로 훨씬 효율적으로 동작할 수 있는 낙관적인 방법이 있다. 일단 값을 변경하고 다른 스레드의 간섭 없이 값이 제대로 변경되는 방법이다. 이 방법에는 충돌 검출 방법을 사용해 값을 변경하는 동안 다른 스레드에서 간섭이 있었는지를 확인할 수 있으며, 만약 간섭이 있었다면 해당 연산이 실패하게 되고 이후에 재시도하거나 아예 재시도조차 하지 않기도 한다. 초기의 프로세서는 확인하고 값 설정(test-and-set), 값을 읽어와서 증가(fetch-and-increment), 치환(swap)등의 단일 연산을 하드웨어적으로 제공했으며, 이런 연산을 기반으로 더 복잡한 병렬 클래스를 쉽게 만드는 데 도움이 되는 Mutex를 구현할 수 있었다. 최근에는 거의 모든 프로세서에서 읽고 -> 변경하고 -> 쓰는 단일 연산(CAS연산)을 하드웨어에서 제공하고 있다. (compare-and-swap, load-linked/store-conditional등의 연산이 있다.) 운영체제와 JVM은 모두 이와 같은 연산을 사용해 락과 여러 가지 병렬 자료 구조를 작성했지만, JAVA 5.0 이전에는 해당 기능을 직접 사용할 수는 없었다.

    CAS(compare-and-swap)연산에 대해서 자세히 알아보자. CAS연산에는 3개의 인자를 넘겨주게된다. 작업할 대상 메모리의 위치(V), 예상하는 기존 값(A), 새로 설정할 값(B). CAS연산은 V위치에 있는 값이 A와 같은 경우에 B로 변경하는 단일 연산이다. 만약 이전 값이 A와 다르다면 아무런 동작도 하지 않는다. 그리고 값을 B로 변경했건 못했건 간에 어떤 경우라도 현재 V의 값을 리턴한다.(compare-and-set이라는 약간 다른 연산의 경우 리턴되는 값은 설정 연산이 성공했는지의 여부라는 점을 참고하자) 만약 여러 스레드가 동시에 CAS연산을 사용해 한 변수의 값을 변경하려고 한다면, 스레드 가운데 하나만 성공적으로 값을 변경할 것이고, 다른 스레드들은 모두 실패한다. 대신 값을 변경하지 못했다고 해서 Lock을 확보하는 것처럼 대기 상태에 들어가는 대신, 값을 변경하지 못했지만 다시 시도할 수 있다고 알림을 받는 셈이다. CAS연산에 실패한 스레드도 대기상태에 들어가지 않기 때문에 스레드마다 CAS연산을 다시 시도할 것인지, 아니면 다른 방법을 취할 것인지, 아니면 아예 아무 조치도 취하지 않을 것인지를 결정할 수 있다. CAS를 활용하는 일반적인 방법은 먼저 V에 들어 있는 A를 읽어내고, A값을 바탕으로 새로운 값 B를 만들어 낸 후 CAS연산을 사용해 V에 들어 있는 A 값을 B값으로 변경하도록 시도한다. 다른 스레드에서 그 사이에 V의 값을 A가 아닌 다른 값으로 변경하지 않은 한 CAS연산이 성공하게 된다. 이처럼 CAS 연산을 사용하면 다른 스레드와 간섭이 발생했는지를 확인할 수 있기 때문에 Lock을 사용하지 않으면서도 읽고-변경하고-쓰는 연산을 단일 연산으로 구현해야 한다는 문제를 간단하게 해결 할 수 있다.

    JVM에서의 CAS 연산 지원

    JAVA 5.0 이전에는 native 코드를 작성하지 않는 한 불가능한 일이었다. JAVA 5.0 이후 부터는 int, long 그리고 모든 객체의 참조를 대상으로 CAS 연산이 가능하도록 기능이 추가되었으며, JVM은 CAS 연산을 호출받았을 때 해당하는 하드웨어에 적당한 가장 효과적인 방법으로 처리하도록 되어있다. CAS 연산을 직접 지원하는 플랫폼(운영체제)의 경우 JAVA 프로그램을 실행할 때 CAS 연산 호출 부분을 직접 해당하는 기계어 코드로 변환해 실행한다. 하드웨어에서 CAS 연산을 지원하지 않는 최악의 경우에는 JVM자체적으로 스핀 락을 사용해 CAS연산을 구현한다. 이와 같은 저수준의 CAS연산은 단일 연산 변수 클래스, 즉 AtomicInteger와 같이 java.util.concurrent.atomic 패키지의 AtomicXxx클래스를 통해 제공한다. java.util.concurrent 패키지의 클래스 대부분을 구현할 때 이와 같은 AtomicXxx클래스가 직간접적으로 사용됐다.

    Non-Blocking 알고리즘

    Lock기반으로 동작하는 알고리즘은 항상 다양한 종류의 가용성 문제에 직면할 위험이 있다. Lock을 현재 확보하고 있는 스레드가 I/O작업 때문에 대기 중이라거나, 메모리 페이징 때문에 대기 중이라거나, 기타 어떤 원인 때문에라도 대기 상태에 들어간다면 다른 모든 스레드(Lock을 획득하기 위해 대기하고 있는)가 전부 대기 상태에 들어갈 가능성이 있다. 특정 스레드에서 작업이 실패하거나 또는 대기 상태에 들어가는 경우에, 다른 어떤 스레드라도 그로인해 실패하거나 대기 상태에 들어가지 않는 알고리즘을 Non-Blocking 알고리즘이라고 한다. 또한 각 작업 단계마다 일부 스레드는 항상 작업을 진행할 수 있는 경우 Lock-Free 알고리즘이라고 한다. 스레드 간의 작업조율을 위해 CAS 연산을 독점적으로 사용하는 알고리즘을 올바로 구현한 경우에는 대기 상태에 들어가지 않는 특성과 Lock-Free 특성을 함께 가지게 된다. 여러 스레드가 경쟁하지 않는 상황이라면 CAS 연산은 항상 성공하고, 여러 스레드가 경쟁을 한다고 해도 최소한 하나의 스레드는 반드시 성공하기 때문에 성공한 스레드는 작업을 진행할 수 있다. Non-Blocking 알고리즘은 Dead-Lock이나 우선 순위 역전(priority-inversion)등의 문제점이 발생하지 않는다.(지속적으로 재시도만 하고 있을 가능성도 있기 때문에 라이브락 등의 문제점이 발생할 가능성이 있기는 하다.) Non-Blocking 알고리즘이 적용된 Non-Blocking Stack을 예로 살펴보자. Non-Blocking 알고리즘이 적용된 자료구조를 Lock-Free 자료구조라고 한다.

    Non-Blocking Stack

    위의 코드는 Non-Blocking Stack의 구현 코드이다. CAS연산을 시작하기 전에 알고 있던 top 항목이 CAS 연산을 시작한 이후에도 동일한 값이었다면 CAS 연산이 성공한다. 반대로 다른 스레드에서 그 사이에 top 항목을 변경했다면 CAS 연산이 실패하며, 현재의 top항목을 기준으로 다시 새로운 Node 인스턴스를 top으로 설정하기 위해 CAS 연산을 재시도한다. CAS 연산이 성공하거나 실패하는 어떤 경우라 해도 스택은 항상 안정적인 상태를 유지한다.

    알고리즘이란?

    정의 : 알고리즘 (라틴어,독일어: Algorithmus,영어:algorithm, 어원 : 페르시아 수학자 알콰리즈미의 이름에서 유래 )이란 어떠한문제를 해결하기 위한 여러 동작들의 모임이다. 유한성을 가지며, 언젠가는 끝나야 하는 속성을 가지고 있다. 수학과 컴퓨터 과학에서 알고리즘이란 작동이 일어나게 하는 내재하는 단계적 집합이다. 알고리즘은 연산, 데이터 진행 또는 자동화된 추론을 수행한다.

    • 어떤 문제를 컴퓨터로 풀기위한 효율적인 절차
    • 문제를 푸는 단계별 절차를 명확하게 기술
    • 어떤 문제를 컴퓨터로 해결하는 방법을 공부함.
    • 특정 프로그래밍 언어나 문법과 무관함.
    • 다양한 문제해결 방법(알고리즘 설계 기법)을 공부함.
    • 알고리즘 문제를 이해하고 효율적으로 해결하는 방법을 공부함
    • 새로운 문제를 만났을 때, 그것을 해결할 수 있는 능력을 배양함

    알고리즘은 다음의 조건을 만족해야 한다.

    입력 : 외부에서 제공되는 자료가 0개 이상 존재한다.

    출력 : 적어도 2개 이상의 서로 다른 결과를 내어야 한다.(즉 모든 입력에 하나의 출력이 나오면 안됨)

    명확성 : 수행 과정은 명확하고 모호하지 않은 명령어로 구성되어야 한다.

    유한성(종결성) : 유한 번의 명령어를 수행 후(유한 시간 내)에 종료한다.

    효율성 : 모든 Lock Free 알고리즘(Non-Blocking 알고리즘) 과정은 명백하게 실행 가능(검증 가능)한 것이어야 한다.

    명확성과 효율성을 위해서 우리는 알고리즘 이론을 배우는 것이다!

    좋은 알고리즘의 분석 기준

    • 정확성 : 적당한 입력에 대해서 유한 시간내에 올바른 답을 산출하는가를 판단.
    • 작업량 : 전체 알고리즘에서 수행되는 가장 중요한 연산들만으로 작업량을 측정. 해결하고자 하는 문제의 중요 연산이 여러개인 경우에는 각각의 중요 연산들의 Lock Free 알고리즘(Non-Blocking 알고리즘) 합으로 간주하거나 중요 연산들에 가중치를 두어 계산
    • 기억 장소 사용량 : 수행에 필요한 저장 공간
    • 최적성 :그 알고리즘보다 더 적은 연산을 수행하는 알고리즘은 없는가? 최적이란 가장 '잘 알려진' 이 아니라 **'가장 좋은'**의 의미이다
    • 복잡도 (점근 표기법 : Big-O Notation) : 입력 자료의 크기가 N일 경우, 계산되는 수행시간의 (매우 중요하므로, 추후에 설명)

    Break Time "벌새 프로젝트"

    상황 설명 : 증권 전산 데이터 센터(캔자스)와 증권 거래 서버(뉴욕)간의 데이터 전송 및 처리를 1ms (0.001초)를 줄일 경우, 하루에 500만 달러 정도의 시세차익을 거둘 수 있음.

    이 경우, HW(캔자스와 뉴욕 간의 직선 거리로 통과하는 지하 통신선)와 이를 연결하는 SW(최대한 빠른 처리를 위한 알고리즘, 여기서는 1ms를 줄이기 위해 알고리즘을 계속해서 보완해나간다.)

    1.1.2 알고리즘의 구성 요소

    • 문제란 해답을 찾으려고 물어보는 질문
    • 파라미터는 문제에서 특정한 값이 지정되어 있지 않은 변수
    • 입력 사례란 문제의 파라미터에 지정된 특정한 값
    • 특정 입력사례의 해답은 해당 파라미터를 입력사례로 질문한 문제의 해답
    • 어떤 문제의 모든 입력 사례에 대해서 해답을 찾아주는 단계별 절차
    • 입력 파라미터에 어떤 입력 사례가 주어지더라도 해답을 찾을 수 있어야 함.

    👍 고등학교 수학과정("순서도와 알고리즘")

    알고리즘에 대한 이해를 돕기 위한 함수의 구성요소 : def(), return

    def() 란? 파이썬에서 함수를 정의할 때는 def문을 사용한다. def는 '정의하다'라는 뜻의 영어 단어 define에서 앞 글자를 딴 것이다. def 문은 다음과 같은 양식으로 작성함

    1. def 예약어로 시작하는 첫 행에는 함수의 이름을 쓴다. 함수의 이름(t식별자 규칙(2.2.4)에 따라 의미를 알 수 있게 짓는다. 함수 이름 뒤에는 괄호를 붙인다.
    2. 함수의 본문은 함수를 호출했을 떄 실행할 파이썬 코드다. 원하는 만큼 여러 행의 코드를 작성할 수 있음.

    return 이란? 반환 및 종료를 의미, return 문을 만나면 현재 함수를 종료하고, 호출한 곳으로 이동, 반환 값이 있을 경우 해당되는 반환값과 함께 호출한 곳으로 이동.

    0 → 정상 종료 -1 → 에러 발생 1이상 → 정상 종료되었으나 다른 인자가 있음을 나타냅니다. -2이하 → 에러 발생했고 구체적으로 어떤 것인지 나타냅니다.

    • 문제 : 어떤 수 x가 n개의 수로 구성된 리스트 S에 존재하는가?
    • 해답 : x가 존재하면 x의 인덱스가, 존재하지 않으면 0에 해답
    • 파리미터 : 정수 n (>0), 리스트 S(인덱스 범위는 1부터 n까지), 원소 x

    입력 사례 : S = [0,10, 7, 11, 5, 13, 8], n = 6, x= 5, x = 9

    입력 사례에 대한 해답 : location = 4, location = 0

    알고리즘 : 모든 S에 대해서 x의 인덱스를 찾아주는 단계별 절차

    S의 첫째 원소에서 시작하여 x를 찾을 때까지(x가 없는 경우 끝까지)

    각 원소를 차례로 x와 비교한다.

    만약, x를 찾으면 x의 인덱스를 리턴하고,

    x를 찾지 못하면 0을 리턴한다.

    Jupyter Notebook을 활용하거나 코드가 막히거나 이해하는 것이 어려운 경우,

    한 줄씩 보는 것이 가장 중요하기 때문에,

    꼭!!꼭. 파이썬 튜터를 써주세요. (python tutor)

    2. 리스트(배열) 원소의 합 구하기

    문제 : n 개의 원소를 가진 리스트(배열) S의 원소의 합을 구하시오.

    해답 : 리스트(배열) S의 모든 원소들의 합

    파라미터 : 리스트 S, 정수 n

    입력 사례 : S = [-1, 10, 7, 11,5, 13, 8], n =6

    알고리즘 : S의 모든 원소를 차례대로 sum에 더하는 절차

    3. 리스트의 정렬 문제

    문제 : n개의 수로 구성된 리스트 S를 비내림차순으로 정렬하시오.

    해답 : S를 비내림차순으로 정렬한 리슽

    입력 사례 : S = [-1, 10, 7, 11, 5, 13, 8]

    입력 사례에 대한 해답 : S' = [-1, 5,7,8,10,11,13]

    알고리즘 : 모든 S에 대해서 S'을 찾아주는 단계별 절차

    (Plus) 복잡도를 확인하기 위한 Big-O 판별법

    미리보기
      복잡도. (점근 표기법 : Big-O Notation)

      : 실행시간이라는 관점에서 알고리즘의 효율을 측정하고, 연산자의 숫자 혹은 연산 과정의 수

      ex) n 이라는 인자가 주여졌을 때, 1부터 n까지를 더하는 함수 sumOfN을 정의

      **T(n)**은 사이즈가 n인 문제를 푸는데 걸리는 시간을 의미, 여기서는 즉 1+n steps를 말한다.

      이 경우, i를 하나씩 증가시켜, n번을 시행하게 된다.

      이 경우는 하나의 for loop에서 n회 시행, 또 다른 for loop에서 n회 시행하여, 총 $n^2$회를 진행하게 된다.

      문제의 사이즈가 커질수록, 최고 차항의 차수가 결과에 가장 많은 영향을 미친다. 이처럼 시간 복잡에 가장 큰 영향을 미치는 차항으로 시간복잡도를 나타내는 것을 Big-O 표기법이라고 한다.

      성공 알고리즘 EA

      코딩테스트 연습 Lock Free 알고리즘(Non-Blocking 알고리즘) - 크레인 인형뽑기 게임

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.

      튜플

      튜플 원소의 순서를 알기 위해서는 적은 원소를 갖는 집합부터 포함시키면 된다.

      다음과 같은 인풋이 들어오는 경우, 각 집합들을 개수 기준으로 정렬시킨다.

      그 후 적은 원소 집합부터 두 집합씩 선택하며 순회한다.

      순회 과정에서 두번째 집합에서 첫번째 집합을 제거하면, 튜플의 원소를 순서대로 알아낼 수 있다.

      (첫번째 원소도 이 과정에 자연스럽게 포함시키기 위해, 공집합을 이용할 수 있다.)

      순서대로 answer에 넣으면 된다.

      1. 문자열의 원소집합들이 개수 순서대로 정렬되어 있어야 현재집합 - 이전집합 연산이 가능하다.

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.

      불량 사용자

      코딩테스트 연습 - 불량 사용자

      개발팀 내에서 이벤트 개발을 담당하고 있는 "무지"는 최근 진행된 카카오이모티콘 이벤트에 비정상적인 방법으로 당첨을 시도한 응모자들을 발견하였습니다. 이런 응모자들을 따로 모아 불량

      완전탐색을 이용하여 해결 가능하다.

      먼저 가능한 모든 경우의 수를 생각해보자.

      응모자 아이디에서 불량 사용자에 대응시켜볼 후보군을 선택한다.

      응모자 아이디 집합에서 불량 사용자 개수만큼 선택하는 모든 조합만큼의 경우가 존재한다.

      어떤 선택된 응모자 아이디 조합에 대하여,

      불량 사용자 집합의 모든 순서와 대응시켜본다.

      이 때 한번이라도 모든 조합이 매칭이 되면 해당 조합은 정답 후보군이 된다. (answer ++)

      모든 순서의 대한 경우의 수는 순열로 표현할 수 있다.

      데카르트 곱으로 표현된 모든 경우의 수는 다음과 같다.

      1. 순열로 만들어진 모든 순서들 중, 최소 하나의 순서는 모든 조합에 대해 만족해야한다.

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.

      호텔 방 배정

      코딩테스트 연습 - 호텔 방 배정

      분할정복을 이용한 방법

      요청된 방 번호와, 실제로 배정받은 방 번호를 매칭시켜 관리한다.

      (요청된적이 없다면 해당 방 번호는 자료구조에서 찾을 수 없을 것이다.)

      어떤 방 번호에 대해 요청을 받으면, 실제로 배정된 방 번호를 반환하는 함수를 생각해보자.

      방 번호에 request 대해 요청을 수행하는 경우,

      해당 방이 배정된적이 없다면 바로 배정시킨다.

      이 경우 요청된 방이 실제 배정된 방이므로 그 값을 그대로 반환한다.

      해당 방이 이미 배정되었다면 그 방을 요청한 사람이 최종적으로 배정받은 방을 찾고,

      그 다음 방에 대해 다시 요청을 시도한다.

      그리고 재귀적으로 요청하여 최종적으로 배정된 방을 다시 갱신한다.

      (배정된적이 없다면 이전 코드에서 return되었을 것이므로, 이 지점에 온다는 것은 이미 배정된 경우만 가능하다.)

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.

      Union-Find를 이용한 방법

      방이 배정 요청이 다음과 같이 들어온다고 생각해보자.

      실제 배정되는 방은 다음과 같아야 할 것이다.

      위 인풋과 같이 1번 방에 대한 요청이 지속적으로 들어오게 되면, 1번 오른쪽으로 어떤 연결된 덩어리가 생기게 된다.

      이 덩어리들을 하나의 집합으로 생각해보자.

      또한 어떤 요청 하나하나가 다 원소가 하나인 집합이라고 생각해보자.

      그렇다면 어떤 요청이 들어왔을 때 다음과 같이 상황을 나눌 수 있다.

      해당 요청이 들어오지 않은 경우,

      요청 방 번호를 하나의 집합으로 만들고, 방을 배정한다.

      그리고 번호 - 1, 번호 + 1 위치의 방들이 배정되어 있다면, 해당 집합들과 합친다.

      해당 요청이 이미 들어온 경우,

      요청 번호에 위치한 집합에서 최종적으로 배정된 방 번호 + 1을 추천번호라고 부르자.

      집합의 추천 번호를 가져와서,

      추천 방 번호를 하나의 집합으로 만들고, 방을 배정한다.

      그리고 번호 - 1, 번호 + 1 위치의 방들이 배정되어 있다면, 해당 집합들과 합친다.

      집합을 찾고 합치는 연산을 수행하기 위해 Union-Find 자료구조를 이용한다.

      그리고 이 경우, 집합의 key로 사용되는 값이 굉장히 커질수도 있는 값이기 때문에,

      Union-Find의 parent를 비롯한 자료구조들을 배열이 아닌 map으로 대체하여 사용한다.

      그리고 모든 가능한 key에 대해 미리 초기화를 할 수 없기 때문에,

      사용될 방에 대해 그때그때 동적으로 초기화를 하도록 개조한다.

      Union-Find에서는 다음과 같은 기능들을 지원해주어야 한다.

      1. 어떤 방 번호에 대해 동적으로 초기화를 해주는 기능

      2. 어떤 방 번호에 대해 그 방이 속한 집합을 찾고, 그 집합의 추천번호를 알아올 수 있는 기능

      어떤 방 번호에 대한 집합의 추천번호는 다음과 같은 과정으로 구할 수 있다.

      추천번호(recommend) = 최종적으로 배정된 방 번호(last) + 1

      최종 갱신 방 번호는 다음과 같은 과정으로 갱신시킨다.

      초기화 시, last = 요청된 방 번호로 세팅

      합치기 시, 두 집합의 last 중 더 큰 값으로 양쪽 last를 갱신

      1. 호텔 방 번호가 Union-Find의 key가 되기 때문에, Union-Find의 parent와 같은 정보들을 map으로 저장해야 한다.

      2. 방 번호와 같은 값이 key로 사용되는 상황에 Union-Find의 초기화를 미리 수행해둘 수 없기 때문에, 동적으로 초기화가 가능한 기능이 필요하다.

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.

      징검다리 건너기

      코딩테스트 연습 - 징검다리 건너기

      [2, 4, 5, 3, 2, 1, 4, 2, 5, 1] 3 3

      이분탐색을 이용한 방법

      어느 한명이 징검다리를 건널 때마다 모든 돌의 값이 1씩 낮아진다.

      어느 N명이 징검다리를 건너면 모든 돌의 값은 N만큼 낮아질 것이다.

      탐색 대상을 이 N의 값으로 바라보면, 이분탐색이 가능하다.

      N값의 최소범위 0부터 N값의 최대범위 200000000에 대해 탐색을 수행한다.

      다음과 같이 mid값을 구한다.

      해당 값에 대하여, 반복문을 돌며 stones[i] - mid

      실패인 경우 start, mid 구간에서 다시 수행한다.

      성공인 경우 mid + 1, end 구간에서 다시 수행한다.

      start와 end가 같아지는 순간의 그 값이 징검다리를 건널 수 있는 최대 사람 수가 된다.

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.

      Union-Find를 이용한 방법

      한 명씩 건널 때마다 돌의 높이는 낮아진다.

      계속 낮아지다 보면 높이가 낮은 돌부터 하나씩 0이 되게 된다.

      그러므로 값이 낮은 돌부터 순서대로 0이 되게 될 것이다.

      값이 낮은 돌부터 하나씩 방문한다고 가정해보자.

      이 값은 다음 순서로 방문될 것이다.

      방문된 돌의 위치는 물에 잠겼다는 표시를 해 둔다. (여기서는 cnts를 이용함)

      여기서 물에 잠긴 돌을 하나의 집합으로 취급한다.

      그리고 방문이 반복되며 어떤 돌이 물에 잠겼을 때, 이 돌 집합을 좌, 우 위치의 집합들과 합친다.

      집합을 합치는 과정에서 현재 집합의 원소 개수를 갱신시켜준다.

      이 과정을 수행하다가, 집합의 크기가 k보다 커지는 경우, 마지막 돌의 높이를 정답으로 저장한다.

      2. 카운트를 체크하고 laststone을 갱신하는 순간은 Union 이후에만 가능한것은 아니다. (AddCount 이후에도 가능)

      알고리즘 튜토리얼. Contribute to insooneelife/AlgorithmTutorial development by creating an account on GitHub.


0 개 댓글

답장을 남겨주세요