{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Choose an Algorithm That Fits
Recognize common algorithm patterns from the promised result, then adapt their state, stopping, ordering, tie, and no-result rules to a concrete task.
python-foundations
problem-solving-algorithms
algorithm-patterns
searching
Course progress
0%
You already know how for, if, lists, dictionaries, and sets behave. The new skill is choosing how those tools should work together when the solution has not been designed for you.
This lesson follows signals sent by lantern towers during a night festival. Keep six questions visible:
- What shape must the result have?
- What state must survive between input items?
- Does every item matter, or may the algorithm stop early?
- What result represents “nothing found”?
- Must input order be preserved?
- Which small boundary would expose a wrong adaptation?
1. Let the promised result suggest a pattern
Different requests need different result shapes:
| Request | Result shape | State that usually persists | Stop early? |
|---|---|---|---|
| Normalize every signal | one output per input | output collection | no |
| Keep only active signals | selected subset | output collection | no |
| Count red signals | one integer | counter | no |
| Add energy readings | one combined value | running total | no |
| Find the first alert | one item or no result | no result / found item | yes |
| Choose the strongest tower | one best item or no result | running best | usually no |
| Count each symbol | key-to-count mapping | frequency dictionary | no |
| Group readings by tower | key-to-values mapping | group dictionary | no |
| Keep first-seen symbols | ordered collection | result plus seen set | no |
| Detect changes | relationship per adjacent pair | previous/current pair | no |
| Find a sync sequence | position or no result | current window | yes |
The table is not a code generator. Words such as best, unique, and find still hide decisions. The contract must specify ties, order, eligibility, and a no-result value.
2. Transform when every input produces one output
Lantern symbols arrive in mixed case:
The output is:
Trace the relationship:
| Input position | Input | Output | Output length |
|---|---|---|---|
| 0 | "r" |
"R" |
1 |
| 1 | "G" |
"G" |
2 |
| 2 | "b" |
"B" |
3 |
| 3 | "R" |
"R" |
4 |
A transformation normally preserves the number and order of items while changing each value. A comprehension expresses the same contract compactly:
This version accidentally filters while transforming:
It is valid only if the contract says some inputs should disappear.
3. Filter when only matching inputs survive
Each reading contains an activation flag:
Keep active records in encounter order:
Filtering asks a yes/no question for every item. The selected records do not need to be changed. A comprehension can remain readable when the predicate is short:
Check empty behavior:
An empty selected subset is not the same shape as None. The contract promised a list, so an empty list is the natural result.
4. Count matches with one explicit counter
Count red symbols without keeping the red symbols themselves:
The counter starts at zero because no input has been processed. Each match adds one. Non-matches leave the state unchanged.
A useful invariant in plain language is:
After each iteration,
red_countequals the number ofRsymbols in the processed prefix.
Do not confuse counting items with adding their values:
5. Accumulate with an initial value that fits the operation
A running total begins at zero:
A trace shows why:
| Energy | Total before | Total after |
|---|---|---|
| 4 | 0 | 4 |
| 9 | 4 | 13 |
| 6 | 13 | 19 |
Joining text has a different practical pattern. Repeated += can work for small text, but collecting pieces and joining once makes the intended separator explicit:
An initial value must match the contract. Zero is suitable for addition, an empty list for collecting results, and None for “no candidate yet.” There is no universal initializer for every accumulation.
6. Search for a first match and stop deliberately
Find the first energy reading at or above an alert threshold:
Run distinct cases:
Returning immediately is correct because later matches cannot replace the first match. The return after the loop handles every no-match path.
Compare three related questions:
any promises a Boolean, all promises a Boolean, and first_alert promises a value or None. The request determines which shape is useful.
Checkpoint: transform, filter, count, accumulate, and search
7. Select the best item with an explicit tie rule
A strongest-tower request cannot usually stop at the first strong reading. A later tower may be stronger.
Trace the running best:
west is ineligible. south replaces north. east ties south but does not replace it because the condition uses > rather than >=; the earlier active record wins.
After understanding the contract, a built-in can express part of it:
max keeps the first maximal item, but the filtering and no-result choice still come from your contract.
8. Build a frequency table for every distinct symbol
Counting only R needs one integer. Counting every symbol needs a mapping from symbol to count:
Trace "ABACA":
| Symbol | Mapping before | Mapping after |
|---|---|---|
A |
{} |
{'A': 1} |
B |
{'A': 1} |
{'A': 1, 'B': 1} |
A |
{'A': 1, 'B': 1} |
{'A': 2, 'B': 1} |
C |
{'A': 2, 'B': 1} |
{'A': 2, 'B': 1, 'C': 1} |
A |
{'A': 2, 'B': 1, 'C': 1} |
{'A': 3, 'B': 1, 'C': 1} |
Check ordinary and empty behavior:
.get(symbol, 0) supplies the count before a symbol has appeared. The stored count then becomes state for later occurrences.
9. Group records when each key owns several values
Frequency counting stores one integer per key. Grouping stores a collection or other aggregate per key.
Use repeated tower names:
repeated_readings = [
{"tower": "north", "energy": 4, "active": True},
{"tower": "south", "energy": 6, "active": True},
{"tower": "north", "energy": 7, "active": True},
{"tower": "south", "energy": 9, "active": False},
]
assert energies_by_tower(repeated_readings) == {
"north": [4, 7],
"south": [6],
}Choose the stored value from the output contract:
- counts per tower → integer;
- energy values per tower → list;
- energy total per tower → running numeric total;
- latest reading per tower → one record.
All use a dictionary, but they are different algorithms because their updates and output shapes differ.
10. Remove duplicates without losing first-seen order
A set can remove duplicates, but a set result does not promise encounter order. When order matters, keep both a membership structure and an output structure:
Check cases that expose the contract:
These structures have different jobs:
seenanswers “has this value appeared?”;resultpreserves “in what order did new values appear?”
Returning list(set(values)) does not express the order promise, even when one small run happens to look right.
Checkpoint: selection, frequencies, grouping, and uniqueness
11. Compare neighbors without losing an endpoint
Some questions concern relationships between adjacent values: Did energy rise? Where did the symbol change? How many equal pairs occur?
An index-based scan names both positions:
Check the first comparable index and small inputs:
Starting at 1 is deliberate: index 0 has no predecessor. A paired traversal can express the values rather than indexes:
Use indexes when the position itself is part of the result. Use paired values when only the relationship matters.
12. Scan a fixed-size window for a local pattern
A synchronization marker is the first consecutive window whose symbols are all distinct. Return the 1-based end position of that window:
Trace "AABCDEF" with width 4:
end |
Slice indexes | Window | Distinct? |
|---|---|---|---|
| 4 | 0:4 |
AABC |
no |
| 5 | 1:5 |
ABCD |
yes |
The function returns 5, meaning that five input symbols have been processed when the marker ends. It does not return the zero-based start index 1.
Check boundaries:
Writing the slice and returned-position meanings beside the trace prevents a common off-by-one error.
13. Combine patterns only when the contract connects them
Suppose the task is: “From active readings, return normalized tower names.” A filter followed by a transform directly matches the words:
A longer sequence may be clearer as named stages:
One loop can combine filter, transform, and accumulate:
Neither version is automatically superior. The staged form exposes reusable intermediate results. The combined form avoids building intermediate lists. Use the contract, scale, and clarity—not a slogan that fewer loops are always better.
14. Built-ins express patterns but do not decide the contract
Python provides concise operations:
Before choosing one, identify the promised result:
sumaccumulates numeric values;minandmaxselect one extreme value;anyandallreturn Booleans and may stop early;sortedreturns a complete ordered list.
A built-in cannot infer eligibility or a product-specific tie rule. Provide those decisions explicitly:
This is concise only after the generator expression, key, and default are understood. A direct loop is preferable when it better exposes the current learning goal or complex tie behavior.
15. Repair patterns chosen from one keyword
“Find all red signals”
A first-match search is wrong because all matching values must appear in the result.
“Return unique signals”
The word unique is ambiguous. It might mean distinct values in first-seen order:
Or it might mean values that occur exactly once:
“Choose the best reading”
Define eligibility, comparison direction, tie rule, no-result behavior, returned shape, and whether every item must be inspected. Only then choose running-best, max, sorting, or another strategy.
Checkpoint: neighbors, windows, combinations, and built-ins
16. Lab: decode the lantern relay
Use this signal and reading sequence:
Implement these contracts:
def count_symbols(signal):
"""Return a frequency dictionary for signal."""
pass
def first_energy_at_least(values, threshold):
"""Return the first qualifying energy value, or None."""
pass
def rising_energy_positions(values):
"""Return indexes whose values are greater than their predecessors."""
pass
def first_distinct_window_end(signal, width):
"""Return the 1-based end of the first distinct window, or None."""
passUse these progressive checks:
assert count_symbols("") == {}
assert count_symbols("RRGYBGRY") == {"R": 3, "G": 2, "Y": 2, "B": 1}
assert first_energy_at_least([], 6) is None
assert first_energy_at_least(relay_energy, 6) == 8
assert rising_energy_positions([]) == []
assert rising_energy_positions([5]) == []
assert rising_energy_positions(relay_energy) == [1, 3, 5, 6]
assert first_distinct_window_end("AAAA", 2) is None
assert first_distinct_window_end("RRGYBGRY", 4) == 5For each function, record:
- promised result shape;
- persistent state;
- stopping rule;
- empty/no-result value;
- order requirement; and
- one boundary that could expose a mistake.
Hint: solve the window on paper
For width four, list candidate windows in order. RRGY repeats R; the next window, RGYB, contains four distinct symbols and ends after five processed symbols.
Show the window evidence
The width-four windows begin RRGY, RGYB, GYBG, YBGR, and BGRY. RGYB is the first all-distinct window and ends after position 5, which agrees with the supplied assertion.
17. Name the state and stopping rule
Complete this table for your lab before checking the summary:
| Function | State | Stops early? | No-result/empty output |
|---|---|---|---|
count_symbols |
|||
first_energy_at_least |
|||
rising_energy_positions |
|||
first_distinct_window_end |
Compare the pattern choices
count_symbols: frequency dictionary; no early stop;{}for empty input.first_energy_at_least: no persistent collection, only current value; stops on a match;Nonefor no match.rising_energy_positions: result list plus adjacent values/index; examines all pairs;[]for fewer than two values.first_distinct_window_end: current candidate window/end position; stops on a match;Nonefor invalid width or no matching window.
Key points
- Start from the promised result shape, then choose state, update, stopping, order, tie, and no-result behavior.
- Transform, filter, count, accumulate, search, select, group, deduplicate, compare neighbors, and scan windows answer different questions.
- Built-ins express familiar algorithms but cannot decide product rules.
- Several clear passes can be better than one overloaded pass; one combined pass can be better when intermediate collections add no value.
- A pattern is only a starting structure. Acceptance examples determine whether its adaptation is correct.