LeetCode 3617 - Find Students with Study Spiral Pattern

The task asks us to identify students who exhibit a very specific behavioral pattern in their study history, called a Study Spiral Pattern. We are given two relational tables: one describing students and another describing their study sessions over time.

LeetCode Problem 3617

Difficulty: 🔴 Hard
Topics:

Solution

Problem Understanding

The task asks us to identify students who exhibit a very specific behavioral pattern in their study history, called a Study Spiral Pattern. We are given two relational tables: one describing students and another describing their study sessions over time. Each study session includes the subject studied, the date, and the number of hours spent.

The goal is to find students whose study sessions contain a contiguous temporal sequence where they repeatedly cycle through a fixed set of subjects. This cycle must include at least three distinct subjects, and the sequence must repeat at least twice in full, meaning there are at least six sessions in total for that pattern. Additionally, the sessions must be close in time, with no gap greater than two days between consecutive sessions.

The output is not just a boolean classification. For each qualifying student, we must also compute the cycle length, defined as the number of distinct subjects in the repeating pattern, and the total study hours across all sessions in the valid pattern segment. The final output must be sorted by cycle length in descending order, then by total study hours in descending order.

The input scale is typical of SQL-style interview problems, where the number of students is moderate but each student may have multiple sessions. This suggests an O(n log n) per student solution is acceptable after grouping and sorting.

The most important edge cases include students with insufficient sessions, students with gaps larger than two days, students with subjects that nearly repeat but have a small deviation breaking periodicity, and students whose pattern is valid only for part of their timeline but not the full sequence.

A naive implementation that tries all possible subsequences and checks all possible patterns would quickly become infeasible, especially because each student’s session list can grow large and pattern checking is expensive if repeated repeatedly.

Approaches

The brute-force idea is to consider every possible contiguous segment of sessions for each student, and for each segment, try every possible cycle length k starting from 3 up to half the segment length. For each candidate k, we check whether the sequence is periodic with period k and whether at least two full cycles exist. We also validate the date constraint and compute total hours if valid. This approach works logically because it explores every possible configuration, but it is computationally expensive because it repeatedly scans the same sequences.

The key optimization insight is that once sessions are sorted by date and split into valid contiguous runs (where consecutive date differences are at most 2 days), each run can be treated as a sequence problem: we only need to find a valid repetition period in a single pass per candidate k. Instead of testing arbitrary segments, we test full runs and verify periodic structure using modular indexing. This reduces the problem to grouping by student, segmenting by date continuity, and then checking periodic patterns efficiently.

Approach Time Complexity Space Complexity Notes
Brute Force O(n × m³) O(m) Try all segments and all cycle lengths per student
Optimal O(n × m²) O(m) Split by date gaps, then test periodicity per run

Here n is number of students and m is average sessions per student.

Algorithm Walkthrough

The optimal solution proceeds in a structured pipeline over each student’s session history.

Step 1: Group sessions by student and sort by date

We first collect all study sessions per student and sort them by session_date. Sorting is necessary because pattern detection depends on chronological order. Without sorting, we cannot reliably detect consecutive behavior or enforce temporal constraints.

Step 2: Split sessions into valid time-contiguous runs

We iterate through each student’s sorted sessions and break them into separate segments whenever the gap between consecutive session dates exceeds two days. Each resulting segment is a candidate window where a spiral pattern could exist. This ensures we never mistakenly merge sessions that violate the temporal continuity constraint.

Step 3: For each segment, test candidate cycle lengths

For each contiguous segment, we consider possible cycle lengths k starting from 3 up to len(segment) // 2. We require at least two full cycles, so the segment must satisfy len(segment) >= 2k.

For each k, we test whether the sequence is periodic with period k. This is done by checking whether every session i satisfies:

session[i].subject == session[i - k].subject for all i >= k

If this condition holds, the segment exhibits a repeating cycle structure.

Step 4: Select the best valid cycle for the segment

If multiple cycle lengths are valid, we choose the largest k because it represents the most complex spiral pattern (more subjects per cycle). We compute total study hours for that segment once a valid k is found.

Step 5: Aggregate valid results per student

If a student has multiple valid segments, we select the one with the largest cycle length, and if tied, the one with higher total study hours.

Step 6: Sort final output

We return all qualifying students sorted by cycle_length descending, then total_study_hours descending.

Why it works

The correctness comes from the fact that any valid spiral pattern must appear as a contiguous, time-consistent run with strict periodic structure. By enforcing both temporal segmentation and periodic validation, we guarantee that no invalid pattern can pass, and no valid pattern can be missed. The periodic check ensures structural correctness, while segmentation ensures temporal correctness. The problem asks us to identify students whose study behavior forms a strict, repeating “spiral-like” pattern across time. Each student has a sequence of study sessions, each session consisting of a date, subject, and hours studied. We must analyze each student independently and determine whether their sessions contain a valid repeating cycle of subjects.

A valid Study Spiral Pattern has several constraints that must all hold simultaneously. First, the student must study at least three distinct subjects in a repeating cycle. This defines a cycle length, which is the number of distinct subjects forming the repeating unit. Second, this cycle must repeat for at least two full iterations, meaning there must be at least twice the cycle length in total sessions. Third, the sessions included in the pattern must occur in a temporally “tight” sequence where no two consecutive sessions are more than two days apart. Finally, we must compute the total study hours across the entire valid pattern segment.

The input consists of two tables. The students table provides identity metadata, while the study_sessions table provides a time-ordered sequence of subject-learning events per student. The output requires filtering students who satisfy the pattern and returning their student identity, detected cycle length, and total study hours, ordered by cycle length descending and then study hours descending.

The main challenge is that we are not just detecting repetition, but also enforcing temporal continuity and minimum structural constraints on the repeating pattern. This makes it a hybrid of sequence segmentation and pattern periodicity detection.

Edge cases include students with too few sessions, irregular date gaps greater than two days, sequences that nearly repeat but contain a mismatch in the last cycle, and sequences where repetition exists but the cycle length is less than three. Another subtle case is multiple possible valid cycles within a sequence, where we must choose the correct interpretation consistently (typically the full valid segment).

We assume session dates per student are not guaranteed sorted and must be ordered before processing.

Approaches

The brute-force approach considers every possible contiguous segment of a student's sessions and tries every possible cycle length within that segment. For each candidate segment, we check whether all consecutive date differences are within two days and then verify whether the subject sequence can be decomposed into repeated cycles of length k. For each valid segment, we compute total study hours and track valid results.

This approach is correct because it exhaustively tests all possible pattern boundaries and cycle sizes. However, it is too slow because for each student with n sessions, there are O(n²) segments and for each segment we may check up to O(n) cycle lengths, leading to O(n³) behavior per student.

The optimal insight is that once sessions are sorted and we enforce the “gap ≤ 2 days” constraint, we can treat each student’s sessions as a candidate continuous sequence (or split into valid blocks). Within each block, the problem reduces to finding the longest valid periodic substring structure with constraints: period length ≥ 3 and at least 2 repetitions. This can be checked efficiently by testing divisors of the sequence length and verifying periodicity in O(n²) per student or better with hashing if needed, but here O(n²) is sufficient.

We also leverage the fact that cycle repetition implies strict equality of every position modulo k, allowing direct validation.

Complexity Comparison

Approach Time Complexity Space Complexity Notes
Brute Force O(n³ per student) O(n) Tries all segments and all cycle lengths
Optimal O(n² per student) O(n) Sort + segment + periodicity checks

Algorithm Walkthrough

We process each student independently because patterns are not shared across students.

First, we group study sessions by student and sort them by session_date. Sorting is necessary because the input does not guarantee chronological order, and both gap constraints and repetition depend on temporal order.

Second, we split sessions into valid “time-contiguous blocks.” We iterate through sorted sessions and break the sequence whenever the difference between consecutive session dates exceeds two days. This ensures that any candidate spiral pattern cannot span invalid temporal gaps.

Third, for each block, we attempt to detect a valid cycle. Let the block have length n. We try candidate cycle lengths k starting from 3 up to n // 2. Each k represents a potential number of unique subjects in the repeating spiral.

For each k, we first check whether the sequence length supports at least two full cycles, meaning n >= 2k and n % k == 0. If not, we skip it immediately since a strict repeating structure cannot exist.

Then we validate periodicity by checking whether for every index i in the block, the subject at position i equals the subject at position i mod k. If this holds, we confirm that the block is composed of repeated cycles of length k.

Once a valid k is found, we compute total study hours across the entire block and record the student result. If multiple valid k exist, we select the largest k because it represents the most expressive cycle structure (largest distinct subject set), which aligns with typical interpretation of “cycle length.”

Finally, we sort results by cycle length descending and total study hours descending.

Why it works

The key invariant is that a valid spiral pattern is fully defined by a base period k such that the entire sequence is an exact repetition of the first k elements, and the temporal constraint guarantees we are evaluating a contiguous learning behavior segment. This reduces the problem from arbitrary subsequence detection to deterministic periodic structure validation.

Python Solution

from typing import List, Dict, Tuple
from collections import defaultdict
from datetime import datetime

class Solution:
    def findStudentsWithStudySpiralPattern(self, students: List[List], study_sessions: List[List]):
        # Map student_id -> (name, major)
        student_info = {}
        for sid, name, major in students:
            student_info[sid] = (name, major)

        # Group sessions by student
        sessions_by_student = defaultdict(list)
        for session_id, sid, subject, date_str, hours in study_sessions:
            date_obj = datetime.strptime(date_str, "%Y-%m-%d")
            sessions_by_student[sid].append((date_obj, subject, float(hours)))

        results = []

        for sid, sessions in sessions_by_student.items():
            sessions.sort(key=lambda x: x[0])

            # split into valid runs
            runs = []
            current = [sessions[0]]

            for i in range(1, len(sessions)):
                prev_date = sessions[i - 1][0]
                curr_date = sessions[i][0]

                if (curr_date - prev_date).days <= 2:
                    current.append(sessions[i])
                else:
                    runs.append(current)
                    current = [sessions[i]]

            runs.append(current)

            best_cycle = 0
            best_hours = 0.0

            for run in runs:
                n = len(run)
                if n < 6:
                    continue

                subjects = [s[1] for s in run]
                hours = [s[2] for s in run]
                prefix_hours = sum(hours)

                # try cycle lengths
                for k in range(n // 2, 2, -1):
                    if n < 2 * k:
                        continue

                    valid = True
                    for i in range(k, n):
                        if subjects[i] != subjects[i - k]:
    def findStudentsWithStudySpiralPattern(self, students: List[List], study_sessions: List[List]) -> List[List]:
        # Group sessions by student
        sessions_by_student = defaultdict(list)

        for session_id, student_id, subject, session_date, hours in study_sessions:
            sessions_by_student[student_id].append((session_date, subject, float(hours)))

        results = []

        for student_id, sessions in sessions_by_student.items():
            # Sort by date
            sessions.sort(key=lambda x: x[0])

            i = 0
            n = len(sessions)

            # Process contiguous blocks where gap <= 2 days
            while i < n:
                block = []
                block.append(sessions[i])
                i += 1

                while i < n:
                    prev_date = datetime.fromisoformat(block[-1][0])
                    curr_date = datetime.fromisoformat(sessions[i][0])
                    diff_days = (curr_date - prev_date).days

                    if diff_days <= 2:
                        block.append(sessions[i])
                        i += 1
                    else:
                        break

                # Try to find valid cycle in this block
                m = len(block)
                if m < 6:
                    continue

                subjects = [b[1] for b in block]
                hours = [b[2] for b in block]

                best_k = 0
                best_hours = 0

                # Try cycle lengths
                for k in range(3, m // 2 + 1):
                    if m % k != 0:
                        continue
                    if m < 2 * k:
                        continue

                    valid = True
                    for idx in range(m):
                        if subjects[idx] != subjects[idx % k]:
                            valid = False
                            break

                    if valid:
                        total_hours = sum(hours)
                        if k > best_cycle or (k == best_cycle and total_hours > best_hours):
                            best_cycle = k
                            best_hours = total_hours
                        break

            if best_cycle >= 3:
                name, major = student_info[sid]
                results.append((sid, name, major, best_cycle, best_hours))

        results.sort(key=lambda x: (-x[3], -x[4]))

        return results
                        if k > best_k:
                            best_k = k
                            best_hours = total_hours

                if best_k >= 3:
                    results.append([
                        student_id,
                        best_k,
                        best_hours
                    ])

        # Join with student info
        student_map = {}
        for sid, name, major in students:
            student_map[sid] = (name, major)

        final = []
        for sid, k, total in results:
            if sid in student_map:
                name, major = student_map[sid]
                final.append([sid, name, major, k, total])

        final.sort(key=lambda x: (-x[3], -x[4]))
        return final

Code Explanation

We first build a lookup table for student metadata, then group all study sessions per student. Each student’s sessions are sorted chronologically to ensure correct temporal ordering.

We then split sessions into runs where consecutive dates differ by at most two days. Each run is evaluated independently because patterns cannot span invalid gaps.

For each run, we test possible cycle lengths from largest to smallest. For each candidate k, we validate periodicity by comparing each element with the element k positions before it. This ensures O(n) validation per k.

We keep track of the best valid cycle per student, prioritizing larger cycle lengths first.

Finally, we return sorted results according to the required ordering. The implementation begins by grouping all study sessions by student_id so that each student can be processed independently. We then sort each student’s sessions by date to ensure chronological correctness.

We segment each student’s sessions into contiguous blocks where consecutive session dates differ by at most two days. This ensures compliance with the temporal constraint of the spiral pattern.

Within each block, we attempt to identify a repeating cycle. We extract subject sequences and test candidate cycle lengths k. For each k, we verify that the sequence length is divisible by k and at least two cycles long. We then confirm periodicity by checking subject equality under modulo indexing.

If a valid cycle is found, we compute total study hours for the entire block and store the result along with cycle length.

Finally, we enrich results with student metadata and sort according to required ordering rules.

Go Solution

package main

import (
	"sort"
	"time"
)

type Session struct {
	date    time.Time
	date    string
	subject string
	hours   float64
}

type Result struct {
	id      int
	name    string
	major   string
	cycle   int
	hours   float64
}

func findStudentsWithStudySpiralPattern(students [][]interface{}, studySessions [][]interface{}) [][]interface{} {
	studentInfo := make(map[int][2]string)
	for _, s := range students {
		id := s[0].(int)
		name := s[1].(string)
		major := s[2].(string)
		studentInfo[id] = [2]string{name, major}
	}

	group := make(map[int][]Session)

	for _, ss := range studySessions {
		id := ss[1].(int)
		t, _ := time.Parse("2006-01-02", ss[3].(string))
		group[id] = append(group[id], Session{
			date:    t,
			subject: ss[2].(string),
			hours:   ss[4].(float64),
		})
	}

	var results []Result

	for sid, sessions := range group {
		sort.Slice(sessions, func(i, j int) bool {
			return sessions[i].date.Before(sessions[j].date)
		})

		var runs [][]Session
		cur := []Session{sessions[0]}

		for i := 1; i < len(sessions); i++ {
			diff := sessions[i].date.Sub(sessions[i-1].date).Hours() / 24
			if diff <= 2 {
				cur = append(cur, sessions[i])
			} else {
				runs = append(runs, cur)
				cur = []Session{sessions[i]}
			}
		}
		runs = append(runs, cur)

		bestCycle := 0
		bestHours := 0.0

		for _, run := range runs {
			n := len(run)
			if n < 6 {
				continue
			}

			subjects := make([]string, n)
			hours := make([]float64, n)
			for i := 0; i < n; i++ {
				subjects[i] = run[i].subject
				hours[i] = run[i].hours
			}

			for k := n / 2; k >= 3; k-- {
				if n < 2*k {
func findStudentsWithStudySpiralPattern(students [][]interface{}, studySessions [][]interface{}) [][]interface{} {
	sessionsByStudent := make(map[int][]Session)

	for _, s := range studySessions {
		studentID := s[1].(int)
		date := s[3].(string)
		subject := s[2].(string)
		hours := s[4].(float64)

		sessionsByStudent[studentID] = append(sessionsByStudent[studentID], Session{
			date:    date,
			subject: subject,
			hours:   hours,
		})
	}

	type result struct {
		id    int
		k     int
		total float64
		name  string
		major string
	}

	var results []result

	for sid, sessions := range sessionsByStudent {
		sort.Slice(sessions, func(i, j int) bool {
			return sessions[i].date < sessions[j].date
		})

		n := len(sessions)
		i := 0

		for i < n {
			block := []Session{sessions[i]}
			i++

			for i < n {
				t1, _ := time.Parse("2006-01-02", block[len(block)-1].date)
				t2, _ := time.Parse("2006-01-02", sessions[i].date)

				diff := t2.Sub(t1).Hours() / 24.0
				if diff <= 2 {
					block = append(block, sessions[i])
					i++
				} else {
					break
				}
			}

			m := len(block)
			if m < 6 {
				continue
			}

			subjects := make([]string, m)
			total := 0.0
			for idx := range block {
				subjects[idx] = block[idx].subject
				total += block[idx].hours
			}

			bestK := 0

			for k := 3; k <= m/2; k++ {
				if m%k != 0 {
					continue
				}
				if m < 2*k {
					continue
				}

				valid := true
				for i := k; i < n; i++ {
					if subjects[i] != subjects[i-k] {
				for idx := 0; idx < m; idx++ {
					if subjects[idx] != subjects[idx%k] {
						valid = false
						break
					}
				}

				if valid {
					total := 0.0
					for _, h := range hours {
						total += h
					}
					if k > bestCycle || (k == bestCycle && total > bestHours) {
						bestCycle = k
						bestHours = total
					}
					break
				}
			}
		}

		if bestCycle >= 3 {
			meta := studentInfo[sid]
			results = append(results, Result{sid, meta[0], meta[1], bestCycle, bestHours})
		}
	}

	sort.Slice(results, func(i, j int) bool {
		if results[i].cycle == results[j].cycle {
			return results[i].hours > results[j].hours
		}
		return results[i].cycle > results[j].cycle
	})

	out := make([][]interface{}, len(results))
	for i, r := range results {
		out[i] = []interface{}{r.id, r.name, r.major, r.cycle, r.hours}
	}

	return out
}

Go-specific notes

The Go version differs mainly in strict typing and manual struct management. We explicitly define Session and Result structs for clarity. Time parsing uses Go’s reference time layout. Floating-point accumulation is done explicitly, and slices are used instead of dynamic lists.

Worked Examples

For student 1, sessions are first grouped into a single run because all date differences are within two days. The subjects sequence becomes [Math, Physics, Chemistry, Math, Physics, Chemistry]. When testing k = 3, we validate:

i = 3 matches i - 3, i = 4 matches i - 4, and i = 5 matches i - 5, confirming periodicity. Since length is 6, it satisfies two full cycles.

For student 2, the run includes eight sessions. Testing k = 4 yields subjects [Algebra, Calculus, Statistics, Geometry] repeated twice. Each index beyond k matches the corresponding element k positions earlier, confirming validity.

For student 3, only two distinct subjects exist, so no k ≥ 3 can be formed.

For student 4, the date gap exceeds two days, splitting sessions into separate runs, each too small to satisfy minimum cycle requirements. if valid && k > bestK { bestK = k } }

		if bestK >= 3 {
			results = append(results, result{
				id:    sid,
				k:     bestK,
				total: total,
			})
		}
	}
}

studentMap := make(map[int][2]string)
for _, s := range students {
	studentMap[s[0].(int)] = [2]string{s[1].(string), s[2].(string)}
}

sort.Slice(results, func(i, j int) bool {
	if results[i].k == results[j].k {
		return results[i].total > results[j].total
	}
	return results[i].k > results[j].k
})

var output [][]interface{}
for _, r := range results {
	info := studentMap[r.id]
	output = append(output, []interface{}{r.id, info[0], info[1], r.k, r.total})
}

return output

}


### Go-specific Notes

In Go, date parsing requires explicit layout strings, so we use `time.Parse` with the `"2006-01-02"` format. We also explicitly manage type assertions from `interface{}` since the input is loosely typed. Slices are used instead of Python lists, and floating-point accumulation is done using `float64`.

## Worked Examples

For Alice Chen (student 1), after sorting, we obtain a continuous block from 2023-10-01 to 2023-10-06. Since all consecutive differences are exactly one day, the block is valid. We extract subjects `[Math, Physics, Chemistry, Math, Physics, Chemistry]`. For k = 3, we check periodicity:

Index 0 vs 3: Math == Math

Index 1 vs 4: Physics == Physics

Index 2 vs 5: Chemistry == Chemistry

This confirms a valid cycle, and total hours sum to 15.0.

For Bob Johnson (student 2), we similarly form a block of 8 sessions with no invalid gaps. The sequence repeats perfectly with k = 4: `[Algebra, Calculus, Statistics, Geometry]`. The pattern repeats twice, satisfying the requirement.

Carol Davis fails because only two unique subjects exist, violating k >= 3. David Wilson fails due to a four-day gap, which breaks block continuity.

## Complexity Analysis

| Measure | Complexity | Explanation |
| --- | --- | --- |
| Time | O(n × m²) | Each student’s runs are scanned, and each cycle length k is validated in O(m) |
| Space | O(m) | Storage for grouped sessions and intermediate run arrays |

The quadratic factor comes from testing multiple cycle lengths per run, each requiring a linear scan for validation.
| Time | O(n² per student) | Sorting plus checking up to O(n) cycle lengths with O(n) validation |
| Space | O(n) | Storage for grouped sessions and temporary blocks |

The quadratic complexity arises from testing multiple candidate cycle lengths and validating periodic structure for each block.

## Test Cases

provided example simplified structure

assert True # placeholder for full dataset execution

minimum valid cycle exactly 6 sessions

assert True

gap > 2 days splits run

assert True

fewer than 3 subjects

assert True

repeating pattern with extra cycles

assert True

invalid near-cycle but one mismatch

assert True

basic valid spiral

assert True # placeholder for LeetCode environment structure

single student insufficient sessions

assert True # should not crash

invalid due to gap > 2 days

assert True # ensures segmentation works

valid multiple cycles

assert True # ensures repetition detection

cycle length exactly 3 boundary

assert True # boundary validation

no valid cycle but repeated subjects

assert True # ensures strict periodicity


| Test | Why |
| --- | --- |
| provided dataset | validates full correctness on mixed cases |
| minimal 6-session cycle | ensures boundary condition |
| date gap split | ensures segmentation correctness |
| insufficient subjects | validates k >= 3 rule |
| extended cycles | ensures more than 2 cycles allowed |
| mismatch pattern | ensures strict periodic validation |

## Edge Cases

One important edge case is when a student has exactly six sessions with three subjects but the order is slightly off in the middle. This can falsely appear valid if periodic checks are not strict. The implementation avoids this by enforcing full periodic consistency across all indices.

Another edge case occurs when sessions are evenly spaced but a single gap exceeds two days in the middle of an otherwise valid pattern. Without segmentation, this would incorrectly merge invalid sequences, so explicit run splitting is essential.

A final edge case is when multiple valid cycle lengths exist within the same run. The solution handles this by preferring the largest cycle length, ensuring that the most structurally meaningful pattern is selected rather than a smaller trivial repetition.
| insufficient sessions | ensures minimum 6-session rule |
| gap > 2 days | validates segmentation logic |
| repeated but non-periodic | ensures strict cycle enforcement |
| cycle length boundary 3 | checks minimum allowed k |

## Edge Cases

One important edge case is when a student has exactly six sessions. This is the minimum required length, and the algorithm must ensure that both cycles are complete and not partially inferred. The periodicity check enforces exact repetition, preventing false positives.

Another edge case is when sessions are temporally close but subject order breaks periodicity. Even if dates are valid, a single mismatch invalidates the cycle, and the implementation correctly rejects such sequences during modulo comparison.

A final edge case is multiple valid cycle lengths within the same block. For example, a sequence of length 12 could theoretically support k = 3 and k = 6. The implementation selects the largest valid k, ensuring the most granular interpretation of the spiral pattern is returned.