-
LeetCode의 어려운 문제들도 제약 조건 솔버를 사용하면 훨씬 쉽게 풀 수 있음
-
복잡한 동적 계획법이나 자체 알고리듬 대신 MiniZinc와 같은 제약 조건 솔버를 활용하여 수학적 최적화 문제를 간단하게 해결 가능함
- 전통적인 프로그래밍 언어는 이런 수학적 최적화 문제를 표현하기 어려움으로 제약 조건 기반 접근법이 강점임
-
예외 상황이나 추가 제약 조건이 등장해도 제약 조건 솔버에서의 모델 변경이 최소화됨
- 런타임 복잡성은 직접 짠 최적화된 알고리듬보다 느릴 수 있지만, 유연성과 개발 생산성 면에서 많은 장점이 있음
제약 조건 솔버로 푸는 LeetCode 문제
올바른 도구의 선택
다양한 최적화/만족 문제
- LeetCode 등에서 흔하게 나오는 수학적 최적화 문제(목적 함수 극대화/극소화 및 여러 제약 조건 포함)는
프로그래밍 언어로 풀 때 저수준 구현 때문에 어렵지만, 제약 조건 솔버에는 적합함
- 예를 들어, 아래 성격의 다양한 문제들이 이에 해당함
예시 1: 최대 주식 차익 문제
- '리스트로 주어진 주식 가격 데이터에서, 한 번 사서 그보다 나중에 팔아 얻을 수 있는 최대 이익을 구하라'
- 전통적으로 O(n²) 브루트포스 또는 O(n) 최적 알고리듬 필요
- MiniZinc에서는 아래와 같이 간단한 제약 조건 문제로 정의 가능
array[int] of int: prices = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8];
var int: buy;
var int: sell;
var int: profit = prices[sell] - prices[buy];
constraint sell > buy;
constraint profit > 0;
solve maximize profit;
예시 2: 특정 수의 덧셈/뺄셈으로 0 만들기 (만족 문제)
- '리스트에서 세 수를 더하거나 빼서 0을 만들 수 있는가?'
- 최적 값이 아닌 만족 가능한 해만 찾으면 됨
- 제약 조건 솔버에서 다음과 같이 표현
include "globals.mzn";
array[int] of int: numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8];
array[index_set(numbers)] of var {0, -1, 1}: choices;
constraint sum(n in index_set(numbers)) (numbers[n] * choices[n]) = 0;
constraint count(choices, -1) + count(choices, 1) = 3;
solve satisfy;
예시 3: 히스토그램에서 최대 직사각형 넓이
- '각 막대의 높이가 주어진 히스토그램에서, 만들 수 있는 가장 큰 직사각형의 넓이를 구하라'
- 전통적으로 복잡한 알고리듬과 다수의 상태 관리 필요
- 제약 조건만으로 간결하게 해법 서술
array[int] of int: numbers = [2,1,5,6,2,3];
var 1..length(numbers): x;
var 1..length(numbers): dx;
var 1..: y;
constraint x + dx <= length(numbers);
constraint forall (i in x..(x+dx)) (y <= numbers[i]);
var int: area = (dx+1)*y;
solve maximize area;
output ["(\(x)->\(x+dx))*\(y) = \(area)"]
제약 조건 솔버 접근이 더 나은가?
-
면접 현장에서 시간 복잡도 등 질문에 약점이 있음
- 제약 조건 솔버는 실행 시간 예측이 힘들며, 맞춤형 최적 알고리듬보다 보통 느림
- 하지만 잘못 구현한 저품질 알고리듬보다는 좋으며, 대부분의 프로그래머가 매번 최적 해법을 직접 짜기는 쉽지 않음
-
실제 강점은 새로운 제약 조건 추가에 대한 유연성임
- 예를 들어, 주식 예시에서 한 번이 아니라 여러 번 사고팔도록 변경할 때 알고리듬 복잡도가 급증
- MiniZinc의 제약 조건 모델에서는 소폭의 코드 변경만으로 훨씬 복잡한 요구사항 수용 가능
include "globals.mzn";
int: max_sales = 3;
int: max_hold = 2;
array[int] of int: prices = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8];
array [1..max_sales] of var int: buy;
array [1..max_sales] of var int: sell;
array [index_set(prices)] of var 0..max_hold: stocks_held;
var int: profit = sum(s in 1..max_sales) (prices[sell[s]] - prices[buy[s]]);
constraint forall (s in 1..max_sales) (sell[s] > buy[s]);
constraint profit > 0;
constraint forall(i in index_set(prices)) (stocks_held[i] = (count(s in 1..max_sales) (buy[s] <= i) - count(s in 1..max_sales) (sell[s] <= i)));
constraint alldifferent(buy ++ sell);
solve maximize profit;
output ["buy at \(buy)\n", "sell at \(sell)\n", "for \(profit)"];
-
온라인의 제약 조건 문제 예시는 Sudoku 등 퍼즐 중심이지만, 실제 비즈니스 로직이나 최적화 요구가 있는 문제에 더 흥미롭고 실용적으로 쓸 수 있음
- 예를 들어, symmetry breaking(대칭성 제거) 같은 고급 최적화 기법 적용 가능성도 높음
마무리 및 참고
- 이 글은 저자의 주간 뉴스레터로, 소프트웨어 역사, 형식 기법, 새로운 기술, 소프트웨어 엔지니어링 이론을 다룸
- 관심 있으면 구독 또는 저자의 메인 웹사이트 참고 가능
- 저자의 신간 "Logic for Programmers" 도 출간 중임