Repository navigation
VeriQuick: MaxSubarray (Kadane 알고리즘) 기여 #35
Replies: 4 comments 2 replies
|
증명에 기여해주셔서 감사합니다. 증명이 잘 작성되어 있는 것을 확인했습니다. 다만 설명 글이 리뷰가 어렵습니다. 내용을 볼때 충분한 검토없이 AI로 생성된 글로 보입니다. 다음과 같은 구체적인 문제점들이 있습니다. 한번 고쳐보시고, 다른 점들도 스스로 검토해보시기 바랍니다.
리뷰어와 다른 독자를 고려해 글을 다시 정리해주시기를 기대합니다. Thanks for the proof contribution, and the proofs are well organized. The fact that it is AI-generated does not necessarily mean it is bad. However, this writing has several problems.
I would appreciate it if you could revise the write-up so that reviewers and readers can follow it more clearly. |
|
안녕하세요, 좋은 피드백 감사합니다. 짚어주신 부분들을 고려하여 글을 수정하였으니, 확인 부탁드립니다. |
|
안녕하세요. 수고 많으셨습니다. 증명은 잘 하신것으로 보이고요. 다만, 제가 느끼기에도 글은 이해하기가 어렵네요. |
|
수정해주신 글 잘 읽었습니다. Minor Points
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
이름 Your Name
권해찬
작업물 Your work
prosyslab/veriquick#5
Detailed Description
VeriQuick - PR #5에서 최대 부분배열 합(Maximum Subarray) 문제를 푸는 Kadane 알고리즘이 올바르다는 것과, 입력 길이$n$ 에 대해 $O(n)$ 시간에 동작한다는 것을 Lean으로 증명했습니다. 또한 이를 위해
#instrument에 필요한 수정 세 가지를 더했습니다.풀고자 한 문제
정수 리스트에서 합이 가장 큰 연속 부분배열의 합을 구하는 문제입니다. 입력이 빈 배열인 경우는 예외로 처리하여
none을 반환하고, 그 외에는 최대 부분배열 합m을some m으로 반환합니다. 이하에서 입력은 빈 배열이 아니라고 가정합니다.문제 정의
최대 부분배열 합 문제의 구현
impl이 올바르다는 것은, 빈 배열에 대해서는none을 반환하고, 그 외의 배열xs에 대해서는xs의 최대 부분배열 합m을some m으로 반환한다는 뜻입니다.이를 토대로 작성한 Lean 정의는 다음과 같습니다.
알고리즘
최대 부분 배열합의 위키백과에 소개된 Kadane 알고리즘을 그대로 구현하였습니다.
해당하는 Lean 구현은 다음과 같습니다.
용어 정의
편의를 위해 이 글에서 사용하는 용어를 앞서 정의하겠습니다.
currentSum과bestSum을 갱신)xs의 부분배열:xs의 비어 있지 않은 연속 부분배열xs의 접미배열:xs의 부분배열이면서xs의 마지막 원소를 포함하는 배열xs의 최대 부분배열 합:xs의 모든 부분배열의 합 중 최대값xs의 최대 접미배열 합:xs의 모든 접미배열의 합 중 최대값올바름 증명
maxSubarray_correct는maxSubarray가 명세Correct를 만족한다는 정리입니다. 증명은 두 단계로 이루어집니다. 먼저 첫째 원소hd가[hd]의 최대 접미배열 합이자 최대 부분배열 합임을 보입니다(maxSuffix_singleton,maxSubarray_singleton). 그다음hd를scan의 시작값으로 삼으면scan이 전체 배열의 최대 부분배열 합을 반환함을 보입니다(scan_correct). 이 두 단계는 아래에서 자세히 설명합니다.지금까지 스캔한 배열을
lst라 할 때,scan_correct는 1)currentSum이lst의 최대 접미배열 합이고 2)bestSum이lst의 최대 부분배열 합이면, 남은 배열xs를 모두 스캔한 결과scan currentSum bestSum xs가lst ++ xs의 최대 부분배열 합이라는 정리입니다. 이 두 조건이 이 알고리즘 증명의 불변식입니다. 모든 원소를 스캔하고 나면bestSum이 곧 문제의 해답입니다.증명은
xs에 대한 귀납법으로 할 수 있습니다. 다음 스캔할 원소hd가 있을 때,lst ++ [hd]에서도 불변식이 성립함을maxSuffix_append와maxSubarray_append로 보인 뒤, 귀납 가설을 적용합니다.보조정리
첫째 원소만 스캔한 경우:
maxSubarray는 첫째 원소x를currentSum과bestSum의 초기값으로scan을 호출하므로, 초기값이 불변식을 만족함을 보여야 합니다.maxSubarray_singleton:[x]의 최대 부분배열 합은x이다.maxSuffix_singleton:[x]의 최대 접미배열 합은x이다.추가로 원소를 스캔한 경우: 불변식이 성립하는
lst에, 다음 원소x를 스캔해도lst ++ [x]에서 불변식이 만족함을 보입니다.maxSuffix_append:lst ++ [x]의 최대 접미배열 합은max x (currentSum + x)이다.maxSubarray_append:lst ++ [x]의 최대 부분배열 합은max bestSum (max x (currentSum + x))이다 (기존 최대 접미배열에x를 붙인 것이 새로운 최대 부분배열이 될 수 있으므로currentSum에 대한 불변식이 필요).시간 복잡도 증명
maxSubarray의 시간 복잡도가 입력 길이명제와 증명을 작성하기에 앞서,
scan_timed와maxSubarray_timed의 입력의 크기를 늘려가며 비용을 관찰하였습니다.이 관찰을 통해$5n + 2$ 만큼 비용이 든다는 $5n + 3$ 의 비용 상한이 있다는
scan_timed는 정확히scan_timed_cost명제를 세웠고,maxSubarray_timed는maxSubarray_timed_cost명제를 세웠습니다.scan_timed_cost는xs에 대한 귀납법으로,maxSubarray_timed_cost는xs에 대한 경우 분석 (case analysis)으로 간단하게 증명할 수 있었습니다.마지막으로,$O(n)$ 임을 보였습니다. 상한 함수를
maxSubarray_timed_isLinear에서maxSubarray_timed의 시간 복잡도가Bound n = n + 1로 두고 다음 명제를 증명했습니다.maxSubarray_timed_cost와omega전략을 이용하여 증명할 수 있습니다.Instrumentation 수정
증명 작업 중 발견한 instrumentation 관련 문제들을 해결하기 위해 다음을 추가/수정하였습니다.
Translate.lean에서max/min을 지원하도록 했습니다. 기존에는Nat/Int의max가 일반 unfold 경로로 빠져 "first-order data result" 오류로 실패했습니다. 이제 인스턴스가 정확히Nat.instMax,Int.instMax,instMinNat,Int.instMin일 때만 비용1로 처리하고, 다른 인스턴스는 이전처럼 거부합니다.Instrumentation.lean에서 구조적 재귀 함수 안의let을 처리하도록 고쳤습니다.fun_induction이let을 지역 정의로 바꾸는데, 자동 생성된 증명의simp_all이 이를 사용하지 않아 값 보존 증명이 실패하고 있었습니다. 문제가 되는simp_all에+zetaDelta를 추가했습니다.TimeM.lean에fst_step_prod(veriquick#2의 수정사항과 동일)와snd_step_prod를 추가했습니다.simp가TimeM.done a를(a, 0)으로 풀고 나면fst_step/snd_step이 더 이상 매칭되지 않는데, 이 보조정리들 덕분에 비용 증명에서TimeM.step이나bind를 직접 풀지 않고도simp [f_timed]만으로 단순화를 이어갈 수 있습니다.가장 어려웠던 점
첫 번째로 어려웠던 점은 아름답고 깔끔한 Lean 증명을 쓰는 것이었습니다. 일반 프로그래밍에서는 지금까지의 경험을 토대로 LLM이 생성한 코드가 지저분한지 깔끔한지 구분할 수 있는 감이 있지만, 아직 낯선 Lean은 그러한 감이 없습니다. Lean은 같은 증명을 하는 방법이 다양해서(
simp,simp only,omega등), 방법이 하나뿐이라면 고민하지 않았을 선택을 계속 해야 했습니다.최소한 깔끔한 증명이라도 작성하기 위해 제가 채택한 원칙은 일관성입니다. 변수명, 증명 구조, 코드 분리 방법을 일관적으로 유지하면, 타인이 코드를 읽을 때 다음에 나올 구조를 예측할 수 있어 가독성이 높아집니다. 이는 일반 프로그래밍을 할 때 흔히 적용하는 원칙입니다. 기여자가 많은 코드베이스의 경우, 각자의 기준이 충돌하여 일관성이 깨지기 쉬워, 통상적으로 컨벤션(convention)을 정해 통일합니다. 저는 여러 사람이 기여하는 VeriQuick에도 증명 스타일 가이드가 있으면 도움이 될 것이라고 생각합니다.
두 번째로 어려웠던 점은 문제를 정의하는 일이었습니다. 정의가 잘못되면 증명을 재작성해야 하기에 처음에 정의를 잘 하는 것은 중요합니다. 그러나 같은 문제라도 허용하는 범위에 따라 여러 가지 정의가 가능합니다(예. 빈 배열을 허용하는가?). 어떤 경우에는 더 강한 명세를 쓸 수도 있습니다. 저는 가장 직관적인 정의를 택했으나, 문제를 "잘" 정의하는 것이 정확히 무엇인가는 아직 의문점이 남습니다.
AI 사용
증명 코드는 대부분 직접 작성했습니다. Claude Code는 증명 아이디어(방향 제시), 디버깅, 리팩토링(코드를 깔끔하게 만드는 일)에 사용했습니다. Instrumentation 수정은 AI가 작성하고 제가 검토했습니다.
이 글을 작성할 시에는 문장 다듬기, 전체적인 오류 검사, 영어 용어 번역, 한국어 전문 -> 영어 번역 용도로 Claude를 사용했습니다.
Detailed Description
In VeriQuick - PR #5, I proved in Lean that Kadane's algorithm for the Maximum Subarray problem is correct and runs in$O(n)$ time in the input length $n$ . I also made three fixes to
#instrumentthat this required.The problem
Given a list of integers, find the sum of the contiguous subarray with the largest sum. An empty input is handled as an exception and returns
none; otherwise the maximum subarray summis returned assome m. From here on, the input is assumed to be non-empty.Problem definition
An implementation
implof the maximum subarray sum problem is correct if it returnsnoneon the empty array and, on any other arrayxs, returns the maximum subarray summofxsassome m.The corresponding Lean definition is:
Algorithm
I implemented Kadane's algorithm, as introduced on Wikipedia's page on the maximum subarray problem, as is.
The corresponding Lean implementation is:
Terminology
For convenience, I define the terms used in this post up front.
currentSumandbestSum).xs: a non-empty contiguous subarray ofxs.xs: a subarray ofxsthat contains the last element ofxs.xs: the value obtained by adding all elements ofxs.xs: the largest sum among all subarrays ofxs.xs: the largest sum among all suffixes ofxs.Correctness proof
maxSubarray_correctstates thatmaxSubarraysatisfies the specificationCorrect. The proof has two steps. First, we show that the first elementhdis both the maximum suffix sum and the maximum subarray sum of[hd](maxSuffix_singleton,maxSubarray_singleton). Then, takinghdas the initial values ofscan, we show thatscanreturns the maximum subarray sum of the whole array (scan_correct). These two steps are explained in detail below.Letting
lstdenote the array scanned so far,scan_correctstates that if 1)currentSumis the maximum suffix sum oflstand 2)bestSumis the maximum subarray sum oflst, then the result of scanning the remaining arrayxs,scan currentSum bestSum xs, is the maximum subarray sum oflst ++ xs. These two conditions are the invariant of this algorithm's proof. Once every element has been scanned,bestSumis the answer to the problem.The proof can be done by induction on
xs. When there is a next elementhdto scan,maxSuffix_appendandmaxSubarray_appendshow that the invariant also holds forlst ++ [hd], and then the induction hypothesis is applied.Lemmas
When only the first element has been scanned:
maxSubarraycallsscanwith the first elementxas the initial values ofcurrentSumandbestSum, so we must show that these initial values satisfy the invariant.maxSubarray_singleton: the maximum subarray sum of[x]isx.maxSuffix_singleton: the maximum suffix sum of[x]isx.When further elements are scanned: for a
lstthat satisfies the invariant, we show that scanning the next elementxkeeps the invariant satisfied forlst ++ [x].maxSuffix_append: the maximum suffix sum oflst ++ [x]ismax x (currentSum + x).maxSubarray_append: the maximum subarray sum oflst ++ [x]ismax bestSum (max x (currentSum + x))(the old maximum suffix extended byxcan become the new maximum subarray, so the invariant oncurrentSumis needed).Time complexity proof
I proved that the time complexity of$O(n)$ in the input length $n$ .
maxSubarrayisBefore writing the statements and proofs, I observed the cost of
scan_timedandmaxSubarray_timedwhile increasing the input size.From these observations, I formulated$5n + 2$ , and $5n + 3$ .
scan_timed_cost, stating thatscan_timedcosts exactlymaxSubarray_timed_cost, stating thatmaxSubarray_timedhas a cost upper bound ofscan_timed_costcould be proved simply by induction onxs, andmaxSubarray_timed_costby case analysis onxs.Finally,$O(n)$ . I set the bound function to
maxSubarray_timed_isLinearshows that the time complexity ofmaxSubarray_timedisBound n = n + 1and proved the following statement.Since$5n + 3 ≤ 5(n + 1)$ , I chose $c = 5, n₀ = 0$ , and the proof follows from
maxSubarray_timed_costand theomegatactic.Instrumentation changes
To resolve instrumentation problems I found while working on the proofs, I added/changed the following.
Translate.lean, I added support formax/min. Previously,maxonNat/Intfell through to the generic unfolding path and failed with a "first-order data result" error. Now it is charged a cost of1only when the instance is exactlyNat.instMax,Int.instMax,instMinNatorInt.instMin, and any other instance is rejected as before.Instrumentation.lean, I fixed the handling ofletinside structurally recursive functions.fun_inductionturns aletinto a local definition, but thesimp_allin the auto-generated proof did not use it, so the value-preservation proof was failing. I added+zetaDeltato thatsimp_all.TimeM.lean, I addedfst_step_prod(identical to the change in veriquick#2) andsnd_step_prod. OncesimprewritesTimeM.done ato(a, 0),fst_step/snd_stepno longer match. Thanks to these lemmas, a cost proof can continue simplifying with justsimp [f_timed], without unfoldingTimeM.steporbindby hand.What was hardest
The first difficulty was writing beautiful and clean Lean proofs. In ordinary programming, my experience lets me tell whether LLM-generated code is messy or clean, but I am still new to Lean and lack that intuition. Lean offers many ways to prove the same thing (
simp,simp only,omega, etc.), so I kept having to make choices that I would not have agonized over if there were only one way.The principle I adopted to write proofs that are at least clean is consistency. By keeping variable names, proof structure, and the way code is split consistent, other readers can predict what comes next, which improves readability. This is a principle commonly applied in ordinary programming as well. In a codebase with many contributors, individual standards tend to clash and consistency is easily broken, so teams usually settle on a coding convention to unify them. I think VeriQuick, which many people contribute to, would also benefit from a proof style guide.
The second difficulty was defining the problem. If a definition turns out to be wrong, the proofs must be rewritten, so getting the definition right at the start is important. However, even for the same problem, many definitions are possible depending on the scope allowed (e.g. are empty arrays allowed?). In some cases, one can also state a stronger specification. I chose the most straightforward definition, but what it means to define a problem "well" remains an open question for me.
AI usage
I wrote most of the proof code by hand. I used Claude Code for proof ideas (guidance), debugging, and refactoring (making the code cleaner). The instrumentation changes were written by AI and reviewed by me.
In writing this post, I used Claude for polishing sentences, overall error checking, translating English terms, and translating the full Korean text into English.
All reactions