SAT
충족가능성 문제 SATisfiability problem
- 명제 논리식이 주어졌을 때, 전체 명제를 참으로 만드는 각 명제의 참/거짓을 찾는 문제
- 줄여서 SAT 문제라 함
- 최초로 발견된 NP 완전 문제(모든 NP 문제는 다항 시간에 SAT 문제로 바꿀 수 있음)
- 동시에 모든 SAT 문제는 최대 3개의 변수만 갖는 3-SAT 문제의 결합으로 다항 시간에 바꿀 수 있음
- 모든 NP 문제 → SAT 문제 → 3-SAT 문제
- 3-SAT 문제도 여전히 NP지만, 빨리 푸는 알고리즘이 집중적으로 개발됨
- NP 문제가 있을 때 충분히 빠른 전용 알고리즘이 없다면, 3-SAT으로 바꿔서 풂
- 최대 2개의 변수만 갖는 2-SAT 문제는 P 문제
SMT Satisfiability Modulo Theories
- SMT(Satisfiability Modulo Theories)
- 논리식이 어떤 이론(Theory) 위에서 참이 될 수 있는지를 판단하는 문제
- → "논리적 제약 조건 + 수학적 이론 = 참 가능한가?"
- SMT가 사용하는 이론의 예시
- 선형 산술: 정수나 실수 계산
- 배열 이론: 배열의 인덱스와 값 관련 제약
- 비트벡터 이론: 하드웨어 및 암호화 모델링
- 함수 이론: 함수 및 등호 관계 추론
SMT 활용 분야
- 프로그램 검증:
- 프로그램이 사양(조건)을 항상 만족하는지 자동으로 증명
- 버그나 보안 결함을 사전에 검출
- 모델 체킹:
- 시스템의 모든 가능한 상태를 논리식으로 표현하여, 오류 상태가 존재하는지를 탐색
- 스케줄링 / 최적화
- 시간표, 자원 배분 등의 문제를 논리식으로 정형화하여 최적해를 구함
- 제약 기반 문제 해결
- 퍼즐, 조합 문제(예: 수학 게임, 논리 퍼즐)를 논리 제약으로 표현하고 해답을 도출
- AI/자동 reasoning
- 복잡한 추리 문제의 해결
Z3
- Z3는 Microsoft Research에서 개발한 고성능 SMT Solver
- 다양한 수학 이론 위에서 주어진 논리 조건이 만족 가능한지(SAT/UNSAT)를 자동으로 판단
- 다양한 이론 지원: 정수, 실수, 배열, 비트벡터, 논리 등 복합 이론 처리 가능
- 모델 생성: 조건을 만족하는 구체적인 값(모델)을 출력함
- 다양한 언어 지원: Python, C++, Java, SMT-LIB 표준
- 강력한 최적화 엔진: 조건을 만족하면서 특정 값을 최소화/최대화 가능
설치:
!pip install z3-solver
임포트:
from z3 import *
산수 문제 풀기
x, y = Ints('x y') # 정수 x, y를 정의
s = Solver() # Solver 초기화
s.add(x > 0, x + y == 10, y < 5) # 조건 추가
s.check() # 조건을 충족하는지 확인
m = s.model() # 해 구하기
m # 해 확인
m.eval(x) # x의 값 얻기
m.eval(y) # y의 값 얻기
논리적 조건 확인
age = Int('age') # 나이
entered = Bool('entered') # 영화관 입장 여부 (True/False)
s = Solver() # Solver 설정
s.add(
Implies(entered, age >= 18), # 규칙: 입장한 사람은 18세 이상이어야 함
And(entered == True, age == 16), # 입장했지만 16세인 경우도 있음
)
s.check() # 검증(unsat → 충족 못함)
PSAT 언어논리 문제
논리 문제
a, b, c, d, e = Bools('A B C D E')
s = Solver()
s.add(
Implies(a, b), # A가 채택되면 B도 채택
Implies(Not(a), And(Not(d), Not(e))), # A가 채택되지 않으면 D와 E 역시 채택되지 않는다
Implies(b, Or(c, Not(a))), # B가 채택된다면, C가 채택되거나 혹은 A는 채택되지 않는다
Implies(Not(d), And(a, Not(c))), # D가 채택되지 않는다면, A는 채택 C는 채택되지 않는다
)
s.check() # 확인 → sat (만족하는 경우가 있음)
s.model() # 만족하는 사례 1개
s.push() # 임시 저장
s.add(Not(a)) # A가 채택되지 않는 경우가 있는가?
s.check() # unsat → 그런 경우는 없음
s.pop() # 임시 저장한 시점으로 되돌리기
# Not(a)를 Not(b), Not(c), Not(d), Not(e)로 바꿔가면서 확인
최적화 (1) 변수
# 비용 행렬
cost = [
[9, 999, 6], # 작업0
[6, 5, 8], # 작업1
[7, 4, 3], # 작업2
]
# 각 작업이 어떤 작업자(0~2)에게 할당되는지를 나타내는 변수
jobs = Ints('job0 job1 job2')
최적화 (2) 조건
opt = Optimize() # 최적화 준비
opt.add(jobs[0] >= 0, jobs[0] <= 2) # job0은 0~2번 작업자에게 할당
opt.add(jobs[1] >= 0, jobs[1] <= 2) # job1도
opt.add(jobs[2] >= 0, jobs[2] <= 2) # job2도
opt.add(Distinct(jobs)) # 할당된 작업자는 모두 달라야함
최적화 (3) 풀기
# 비용
costs = []
for j in range(3):
for w in range(3):
costs.append(If(jobs[j] == w, cost[j][w], 0)) # j번 작업을 w가 할 때 비용
total_cost = Sum(costs) # 비용 합계
opt.minimize(total_cost) # 비용 합계가 최소가 되도록
opt.check() # 만족하는가?
m = opt.model() # 만족하는 사례
m # 결과 보기
m.eval(total_cost) # 비용