Structs & Data Structures · intermediate · ~15 min
One-dimensional 'best ending here' DP.
Two array problems share one idea: define dp[i] as the best answer that ends exactly at index i, then combine or restart at each step. Kadane's algorithm finds the maximum contiguous subarray sum by asking, at every element, whether extending the running sum beats starting fresh. Longest increasing subsequence asks, for each element, which earlier smaller element gives the longest chain to extend. The "ending at i" framing is what makes both tractable — and the global answer is then the best over all i, not simply the last entry.
Kadane's is the standard way to find the best window in a stream of gains and losses — peak trading intervals, best signal segment, most profitable period — in a single pass. LIS underlies patience sorting, version-compatibility chains, and any "longest chain of improving measurements" query. Both are also common interview problems precisely because the "ending at i" insight is not obvious from the brute force.
Kadane's recurrence. cur = max(a[i], cur + a[i]) — either the best subarray ending here starts at i, or it extends the previous one. Track best = max(best, cur) as you go.
Initialise from a[0], not 0. Seeding best = 0 reports 0 for an all-negative array, when the correct answer is the least-negative element. This is the classic Kadane bug, and the reason the implementation starts best = cur = a[0] and loops from index 1.
LIS recurrence. dp[i] = 1 + max(dp[j]) over all j < i with a[j] < a[i], defaulting to 1 (the element alone). O(n^2).
The answer is a maximum, not the last cell. For LIS the longest chain may end anywhere, so the result is the max over all dp[i] — returning dp[n-1] is wrong.
Strict vs non-strict. a[j] < a[i] gives strictly increasing; <= allows equal values and yields a different (non-decreasing) answer. Decide which the problem means.
Faster LIS. An O(n log n) variant keeps the smallest possible tail for each length and binary-searches it. Learn the O(n^2) form first — it is the reference you validate the clever one against.
/* maximum contiguous subarray sum - O(n), one pass */
long long kadane(const int *a, int n) {
long long best = a[0], cur = a[0]; /* seed from a[0], NOT 0 */
for (int i = 1; i < n; i++) {
long long ext = cur + a[i];
cur = (a[i] > ext) ? a[i] : ext; /* restart vs extend */
if (cur > best) best = cur;
}
return best;
}
/* longest strictly increasing subsequence - O(n^2) */
int lis(const int *a, int n) {
if (n <= 0) return 0;
int *dp = malloc((size_t)n * sizeof *dp);
if (!dp) return -1;
int best = 1;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++)
if (a[j] < a[i] && dp[j] + 1 > dp[i]) dp[i] = dp[j] + 1;
if (dp[i] > best) best = dp[i]; /* answer is the MAX over all i */
}
free(dp);
return best;
}
Key points:
a[0]; guard n <= 0 before touching it.dp[i].long long for the sum — 32-bit sums overflow on large arrays.Two 1-D array DPs: the longest increasing subsequence (dp[i] = best chain ending at i) and Kadane's maximum subarray (running best contiguous sum). Both compute a per-index answer that extends earlier ones.
#include <stdio.h>
#include <stdlib.h>
static int lis(const int*a,int n){if(n<=0)return 0;int*dp=malloc(n*sizeof(int));int best=1;for(int i=0;i<n;i++){dp[i]=1;for(int j=0;j<i;j++)if(a[j]<a[i]&&dp[j]+1>dp[i])dp[i]=dp[j]+1;if(dp[i]>best)best=dp[i];}free(dp);return best;}
static long long kadane(const int*a,int n){long long best=a[0],cur=a[0];for(int i=1;i<n;i++){long long e=cur+a[i];cur=a[i]>e?a[i]:e;if(cur>best)best=cur;}return best;}
int main(void){
int a[]={10,9,2,5,3,7,101,18};
printf("longest increasing subsequence length = %d\n", lis(a,8));
int b[]={-2,1,-3,4,-1,2,1,-5,4};
printf("max contiguous subarray sum = %lld\n", kadane(b,9));
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | best = cur = a[0] |
For {-2,1,-3,4,-1,2,1,-5,4} both start at -2. |
| 2 | i=1: ext = -2+1 = -1 |
a[1]=1 beats -1, so cur = 1 — the subarray restarts here. |
| 3 | i=2: ext = 1+(-3) = -2 |
-2 beats a[2]=-3, so cur = -2; extending still beats restarting. |
| 4 | i=3: ext = -2+4 = 2 |
a[3]=4 beats 2, restart again; cur = 4, best = 4. |
| 5 | i=4..6 |
cur climbs 3, 5, 6; best reaches 6 — the subarray {4,-1,2,1}. |
| 6 | all-negative input | Because best was seeded from a[0], {-3,-1,-2} correctly returns -1, not 0. |
Requiring contiguity for LIS; resetting Kadane's sum to 0 on all-negative input.
Compiler errors and warnings:
-Wsign-compare between int i and a size_t length; keep the index int.best = 0 seed bug — only an all-negative test catches it.Runtime symptoms:
best = 0. Seed from a[0].a[0] was read without a n <= 0 guard.a[j] > a[i]), or you compared dp values instead of array values.dp[n-1] instead of the maximum over all dp[i].int overflow — accumulate in long long.Technique: test both with three inputs: an all-positive array, an all-negative array, and a single element. Those three catch nearly every seeding and boundary bug.
a[0] before validating n. Kadane's seed dereferences the first element; a zero-length array makes that an out-of-bounds read. Guard n <= 0 first and decide what an empty input should return.malloc(n * sizeof *dp) — validate n > 0, cast to size_t, and check for NULL.malloc vs calloc. The LIS table is fully written (dp[i] = 1) before it is read, so malloc is correct here; had the inner loop read dp[i] first, it would be reading garbage.int values into an int is undefined on overflow. Use long long, and consider the range of the input.n <= 0 return.Concrete uses: Kadane's finds the most profitable contiguous period in a series of daily gains and losses, the strongest segment of a signal, or the best-scoring region in a genome scan. LIS appears in patience sorting, in computing the longest chain of compatible versions or events, and as a subroutine in diff-like tools. Both are single-pass or near-single-pass, which matters for streaming data.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) Kadane's sum. Implement long long kadane(const int *a, int n). Requirements: correct for all-negative input; guard n <= 0. Example: {-2,1,-3,4,-1,2,1,-5,4} -> 6; {-3,-1,-2} -> -1. Concepts: extend-or-restart, seeding.
2. (Beginner) LIS length. Implement the O(n^2) int lis(const int *a, int n). Example: {10,9,2,5,3,7,101,18} -> 4. Concepts: best-ending-at-i, max over all i.
3. (Intermediate) Report the window. Extend Kadane's to also return the start and end indices of the best subarray. Hint: record a candidate start whenever you restart. Concepts: tracking provenance in a DP.
4. (Intermediate) Strict vs non-strict. Produce both LIS variants and show an input where they differ. Example: {1,2,2,3} -> 3 strict, 4 non-decreasing. Concepts: specifying the comparison precisely.
Both problems come from defining dp[i] as the best answer ending at index i. Kadane's asks at each element whether to extend the running sum or restart from that element, seeding from a[0] so an all-negative array returns its least-negative element rather than 0. LIS takes the longest chain among earlier smaller elements and adds one, defaulting to 1 — and the answer is the maximum over all dp[i], never just the last entry. Guard the empty input before reading a[0], accumulate sums in long long, and be explicit about whether "increasing" is strict.