DEV Community

Aaron Elligsen
Aaron Elligsen

Posted on • Updated on

Verify Contains Duplicate II

Verify LeetCode Question: 219. Contains Duplicate II

Following from our last post, we look at slightly more complicated version of the problem.

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) <= k.

An implementation

Here is how we might solve this in JavaScript/TypeScript.

function containsNearbyDuplicate(nums: number[], k: number): boolean {
    let windowSet: Set<number> = new Set();
    const n = nums.length;
    if(k == 0) return false;
    for(let i = 0; i < n; i++) {
        if(windowSet.has(nums[i])) {
            return true;
        }
        if(i >= k) {
            windowSet.delete(nums[i-k]);
        }
        windowSet.add(nums[i]);
    }
    return false;
};
Enter fullscreen mode Exit fullscreen mode

In short, we create a set of numbers again and add elements of the array to it as we iterate. However, once we have moved past the k-th index then we start removing the (i-k)-th elements from the list. If we do this every subsequent step it ensures that only the last k array elements are in the set and then if we encounter an element already in that set we know that it is a duplicate which is in an index position x such that x-i <= k.

Specifying the implementation

Again we convert the problem definition into the specification clause into specification clauses on our Dafny method.

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) <= k.

requires k <= |nums|
ensures containsDuplicate ==> exists i,j :: 0 <= i < j < |nums| && j-i <= k && nums[i] == nums[j]
Enter fullscreen mode Exit fullscreen mode

The difference here from the definition is that we assume one index i will be less than the than the other matching index. Thus the difference j-i will always be positive, so we can skip using the absolute difference.

Requires

requires is another specification clause used by Dafny to impose limits on the inputs allowed for a given method/function. In this case we can read the LeetCode problem constraints and see that the value k provided never exceeds the maximum length of the array. Thus we encode it as a constraint in our method. It makes some logical sense as well in this problem.

Verifying the implementation

Here is a verifying implementation which is a straight forward conversion of the TypeScript code.

// {:verify true} is an attribute that lets us toggle 
//verifying the method until we are ready
method {:verify  true} containsNearbyDuplicate(nums: seq<int>, k: nat) returns (containsDuplicate: bool) 
    requires k <= |nums|
    ensures containsDuplicate ==> exists i,j :: 0 <= i < j < |nums| && j-i <= k && nums[i] == nums[j]
{
    var windowSet: set<int> := {};
    if k == 0 {
        return false;
    }

    for i: nat := 0 to |nums| 
        invariant i < k ==> forall x :: x in windowSet ==> x in nums[0..i] 
        invariant i >= k ==> forall x :: x in windowSet ==> x in nums[i-k..i]
        // invariant if i < k then forall x :: x in windowSet ==> x in nums[0..i] else forall x :: x in windowSet ==> x in nums[i-k..i]

    {
        if nums[i] in windowSet {
            return true;
        }
        windowSet := if i >= k then (windowSet -{nums[i-k]}) + {nums[i]} else windowSet + {nums[i]};
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

Loop invariants choices

Unlike the simpler version of the problem we have two regimes for our loop variable i, while it is less than k and when it is greater than or equal to k.

In Dafny you can specify multiple loop invariants, simply meaning they are additional facts that remain true as a loop iterates.

Here we can see in the loop invariant that we can write either. The loop invariant as two lines using implication.

invariant i < k ==> forall x :: x in windowSet ==> x in nums[0..i] 
invariant i >= k ==> forall x :: x in windowSet ==> x in nums[i-k..i]
Enter fullscreen mode Exit fullscreen mode

Or

invariant if i < k then forall x :: x in windowSet ==> x in nums[0..i] else forall x :: x in windowSet ==> x in nums[i-k..i]
Enter fullscreen mode Exit fullscreen mode

These are both good definitions, but another good way to deal with complicated invariants is to move them into a predicate function. This is often a good strategy to try because of the concept of Triggers in Dafny, which often helps dafny recognize and prove quantifier expressions, like forall and exists.

predicate windowSetValid(nums: seq<int>, k: nat, i: nat, windowSet: set<int>) 
    requires 0 <= i <= |nums|
    requires 0 < k <= |nums|
{
    if i < k then forall x :: x in windowSet ==> x in nums[0..i]
    else forall x :: x in windowSet ==> x in nums[i-k..i]
}
Enter fullscreen mode Exit fullscreen mode

Predicates

predicate functions are a subset of Dafny functions (with all the properties that entails) that only return a boolean value. Unlike a method or a function we do not need to specify a return type. The predicate keyword demands it be a bool.

However, if we replace our loop invariants with the following Dafny will no longer verify, even though the predicate function body on its own will verify.

invariant windowSetValid(nums, k, i, windowSet)
Enter fullscreen mode Exit fullscreen mode

This is a case where Dafny's automatic induction fall down and we have to write a lemma to explain additional facts to Dafny.

Lemmas

lemma is like a function in Dafny, it can take parameters with require and ensure clauses, but it is only existing within the specification context and cannot be compiled to running code. It is technically a proof.

Here we specify all the things which should be true when we reach the positive if case in the loop. Then we show that if we assume those conditions are true then inside the lemma body we show that it implies the ensure clauses is true.

We can then call that lemma in other methods or functions or lemma when the required conditions are met and then Dafny can show that the ensure conditions are valid. This is basically just implication, P(x) ==> Q(x), and P(x) is true then Q(x).

lemma windowSetValidIsSufficient(nums: seq<int>, k: nat, i: nat, windowSet:set<int>)  
    requires 0 <= i < |nums|
    requires 0 < k <= |nums|
    requires windowSetValid(nums, k, i, windowSet)
    requires nums[i] in windowSet
    ensures exists i,j :: 0 <= i < j < |nums| && j-i <= k && nums[i] == nums[j]
{
    if i < k {
        assert nums[i] in windowSet;
        assert forall x :: x in windowSet ==> x in nums[0..i];
    }else{
        assert nums[i] in windowSet;
        assert forall x :: x in windowSet ==> x in nums[i-k..i];
    }
}
Enter fullscreen mode Exit fullscreen mode

Assert

One of the most important tools in developing a Dafny specification is the assert operator. This has two purposes, the first is to query Dafny and see what it believes is true at a given point in a specification. Secondly, it can show Dafny that something is true and then it can use that fact to make other deductions.

Use assert often to see if what should be obvious actually is. Quite frequently Dafny will not believe you unless you show something is true using a sequence of assertions, or a calculation (another syntax construct), or a lemma.

In the above lemma, we assert the conclusions of the windowSetValid predicate being true and then Dafny sees ensure condition is valid and our method verifies when we call the lemma in the method.

This version of the method verifies using the predicate and lemma.


method {:verify  true} containsNearbyValid(nums: seq<int>, k: nat) returns (containsDuplicate: bool) 
    requires k <= |nums|
    ensures containsDuplicate ==> exists i,j :: 0 <= i < j < |nums| && j-i <= k && nums[i] == nums[j]
{
    var windowSet: set<int> := {};
    if k == 0 {
        return false;
    }

    for i: nat := 0 to |nums| 
        invariant windowSetValid(nums, k, i, windowSet)
    {
        if nums[i] in windowSet {
            windowSetValidIsSufficient(nums, k, i, windowSet);
            return true;
        }
        windowSet := if i >= k then (windowSet -{nums[i-k]}) + {nums[i]} else windowSet + {nums[i]};
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)