Integrierte Stichwahl

Aufgabe

Sie kennen bereits die Mehrheitswahl, bei der ein sehr einfacher Algorithmus zur Ermittlung des Wahlsiegers verwendet wird: Jeder Wähler hat eine Stimme und der Kandidat mit den meisten Stimmen gewinnt.

Die Mehrheitswahl hat aber auch Nachteile. Was passiert zum Beispiel bei einer Wahl mit drei Kandidaten, wenn folgende Stimmzettel abgegeben werden?

Fünf Wahlgänge, Gleichstand zwischen Alice und Bob

Eine Mehrheitswahl würde hier zu einem Gleichstand zwischen Alice und Bob führen, da jeder zwei Stimmen hat. Aber ist das das richtige Ergebnis?

Es gibt noch eine andere Art von Wahlsystem, das so genannte Präferenzwahlsystem (auch bekannt als “ranked-choice”). Bei der Präferenzwahl können die Wähler für mehr als einen Kandidaten stimmen. Anstatt nur für ihren Spitzenkandidaten zu stimmen, können sie die Kandidaten in der Reihenfolge ihrer Präferenzen anordnen. Die Stimmzettel könnten also folgendermaßen aussehen:

Fünf Stimmzettel, mit geordneten Präferenzen

In diesem Fall hat jeder Wähler nicht nur seinen Erstpräferenzkandidaten, sondern auch seine Zweit- und Drittpräferenz angegeben. Nun könnte es bei einer zuvor unentschiedenen Wahl einen Sieger geben. Ursprünglich gab es einen Gleichstand zwischen Alice und Bob, so dass Charlie aus dem Rennen war. Der Wähler, der Charlie gewählt hat, hat jedoch Alice gegenüber Bob bevorzugt, so dass Alice zum Sieger erklärt werden könnte.

Durch die Angabe einer Stimmpräferenz kann ein weiterer potenzieller Nachteil der Mehrheitswahl behoben werden. Werfen Sie einen Blick auf die folgenden Stimmzettel:

Neun Stimmzettel, mit geordneten Präferenzen

Wer sollte diese Wahl gewinnen? Bei einer Mehrheitswahl, bei der jeder Wähler nur seine erste Präferenz wählt, gewinnt Charlie die Wahl mit vier Stimmen gegenüber drei für Bob und zwei für Alice. Eine Mehrheit der Wähler (5 von 9) wäre jedoch mit Alice oder Bob anstelle von Charlie glücklicher. Ein Wahlsystem, das die Rangfolge der Präferenzen berücksichtigt, kann einen Sieger ermitteln, der die Präferenzen der Wähler besser widerspiegelt.

Eine Möglichkeit für ein solches Präferenzwahlsystem ist die Integrierte Stichwahl (in Englisch Instant-Runoff-Voting). Bei der integrierten Stichwahl können die Wähler beliebig viele Kandidaten nach ihren Präferenzen ordnen. Erhält ein Kandidat die Mehrheit (mehr als 50%) der Erstpräferenzstimmen, wird er zum Sieger der Wahl erklärt.

Erreicht kein Kandidat mehr als 50% der Stimmen, kommt es zu einer “sofortigen Stichwahl”. Der Kandidat, der die wenigsten Stimmen erhalten hat, scheidet aus der Wahl aus, und bei allen Wählern, die diesen Kandidaten ursprünglich als erste Präferenz gewählt hatten, wird nun die zweite Präferenz berücksichtigt. Was ist der Hintergrund dieses Verfahrens? Im Prinzip wird damit simuliert, was passiert wäre, wenn der am wenigsten beliebte Kandidat gar nicht erst zur Wahl angetreten wäre.

Dieses Verfahren wird wiederholt: Wenn kein Kandidat die Mehrheit der Stimmen erhält, scheidet der letztplatzierte Kandidat aus und von denjenigen, die für ihn gestimmt haben, wird stattdessen die nächste Präferenz berücksichtigt (die nicht selbst bereits ausgeschieden ist). Sobald ein Kandidat eine Mehrheit hat, wird er zum Sieger erklärt.

Klingt etwas komplizierter als eine Mehrheitswahl, oder? Aber es hat wohl den Vorteil, dass der Sieger der Wahl die Präferenzen der Wähler genauer repräsentiert.

Erstellen wir also ein Programm, das eine Stichwahl simuliert. Beachten Sie die Anweisungen in den folgenden Abschnitten.

Demo

Aufgabenmaterial

Für diese Aufgabe werden Sie ein von uns zur Verfügung gestelltes Codegerüst vervollständigen.

Aufgabenmaterial herunterladen

Öffnen Sie VS Code entsprechend Ihrem Setup.

Öffnen Sie Ihr Terminalfenster und führen Sie dann cd aus. Die Eingabeaufforderung Ihres Terminalfensters sollte wie folgt aussehen:

$

Führen Sie dann

wget https://inf.zone/download/exercises/03/runoff.zip

aus, um eine ZIP-Datei namens runoff.zip in den aktuellen Ordner herunterzuladen. Achten Sie darauf, dass Sie das Leerzeichen zwischen wget und der folgenden URL nicht übersehen, und auch kein anderes Zeichen!

Führen Sie jetzt

unzip runoff.zip

aus, um das ZIP-Archiv in einen Ordner namens runoff zu extrahieren. Sie brauchen die ZIP-Datei nicht mehr, also können Sie

rm runoff.zip

ausführen. Antworten Sie mit “y”, gefolgt von der Eingabetaste, um die heruntergeladene ZIP-Datei zu entfernen.

Führen Sie dann

cd runoff

aus, um in dieses Verzeichnis zu wechseln. Ihre Eingabeaufforderung sollte nun wie folgt aussehen:

runoff/ $

Wenn alles funktioniert hat, sollten Sie nach dem Ausführen von

ls

eine Datei mit dem Namen runoff.c sehen. Wenn Sie code runoff.c ausführen, sollte sich die Datei öffnen. In diese Datei werden Sie Ihren Code für diese Aufgabe einfügen. Wenn diese Datei nicht angezeigt wird, verfolgen Sie Ihre Schritte zurück und schauen Sie, ob Sie herausfinden können, wo Sie einen Fehler gemacht haben!

Überblick über runoff.c

Wenn Sie die Funktionalität eines bestehenden Codes erweitern wollen, sollten Sie ihn zunächst in seinem jetzigen Zustand verstehen.

Sehen wir uns zuerst den Anfang von runoff.c an. Dort sind zwei Konstanten definiert: MAX_CANDIDATES für die maximale Anzahl der Kandidaten bei der Wahl und MAX_VOTERS für die maximale Anzahl der Wähler bei der Wahl.

// Max voters and candidates
#define MAX_VOTERS 100
#define MAX_CANDIDATES 9

Die Konstante MAX_CANDIDATES wird verwendet, um die Größe eines Arrays, candidates, festzulegen. Dieses Array enthält alle Kandidaten der Wahl.

// Array of candidates
candidate candidates[MAX_CANDIDATES];

candidates ist ein Array des Typs candidate. In runoff.c ist ein candidate eine eigens definierte struct. Jeder candidate besteht aus einem string für den name, einem int, in dem die Anzahl der votes gespeichert wird, und einem bool-Wert namens eliminated, der angibt, ob der Kandidat bereits aufgrund geringer Stimmenzahl von der Wahl ausgeschlossen wurde.

// Candidates have name, vote count, eliminated status
typedef struct
{
    string name;
    int votes;
    bool eliminated;
}
candidate;

Dies hilft uns, das zweidimensionale Array preferences besser zu verstehen.

// preferences[i][j] is jth preference for voter i
int preferences[MAX_VOTERS][MAX_CANDIDATES];

Das Array preferences[i] enthält alle Präferenzen des Wählers Nummer i. Jedes Element dieses Arrays repräsentiert also gewissermaßen den Stimmzettel eines Wählers. Deshalb ist dieses Array auch so groß wie die Konstante MAX_VOTERS, mit der die maximale Anzahl der Wähler definiert wurde. Der Integer von preferences[i][j] speichert dann den Index des Kandidaten aus dem Array candidates, der die j-te Präferenz des Wählers i ist. Während man also über preferences[i] auf die Stimmzettel der Wähler zugreifen kann, enthält preferences[i][j] gewissermaßen den Inhalt des jeweiligen Stimmzettels. Wenn an der ersten Stelle, also unter preferences[i][0], etwa eine 2 gespeichert ist, bedeutet dies, dass die erste Präferenz des Wählers i, der Kandidat ist, der im Array candidates an dritter Stelle, dem Index 2, gespeichert ist. Dieses Array entspricht daher der Größe der MAX_CANDIDATES-Konstante.

Das Programm hat außerdem zwei globale Variablen: voter_count und candidate_count.

// Numbers of voters and candidates
int voter_count;
int candidate_count;

Nun zur main-Funktion. Hier beginnt nach der Bestimmung der Anzahl der Kandidaten und der Anzahl der Wähler die Schleife zur Wahl. Jeder Wähler bekommt nun die Möglichkeit, seine Stimme abzugeben. Während der Wähler seine Präferenzen eingibt, wird die Funktion vote aufgerufen, um alle Präferenzen zu erfassen. Wird der Stimmzettel zu irgendeinem Zeitpunkt als ungültig angesehen, wird das Programm beendet (also die gesamte Wahl abgebrochen).

Sobald alle Stimmen eingegangen sind, beginnt eine weitere Schleife: In dieser Schleife wird der Sieger der Stichwahl ermittelt und der letztplatzierte Kandidat gestrichen, bis ein Sieger feststeht.

Der erste Aufruf ist hierbei eine Funktion namens tabulate, die alle Präferenzen der Wähler betrachtet und die aktuellen Stimmensummen berechnet, indem sie den Spitzenkandidaten jedes Wählers betrachtet, der noch nicht ausgeschlossen wurde. Danach sollte die Funktion print_winner den Gewinner ausgeben, wenn es einen gibt; wenn es einen gibt, wird das Programm beendet. Andernfalls muss das Programm die geringste Anzahl der Stimmen ermitteln, die jeder Kandidat, der noch zur Wahl steht, erhalten hat (durch einen Aufruf von find_min). Wenn sich herausstellt, dass alle Kandidaten die gleiche Anzahl an Stimmen erhalten haben (wie durch die Funktion is_tie ermittelt), wird die Wahl als unentschieden erklärt; andernfalls wird der Kandidat (oder die Kandidaten) auf dem letzten Platz durch einen Aufruf der Funktion eliminate von der Wahl ausgeschlossen.

Wenn Sie etwas weiter unten in der Datei nachsehen, werden Sie feststellen, dass die Implementierung der Funktionen - vote, tabulate, print_winner, find_min, is_tie und eliminate - Ihnen überlassen ist!

Zusätzlicher Hinweis: Sie sollten nichts in runoff.c ändern, außer den Implementierungen der genannten Funktionen (und dem Hinzufügen zusätzlicher Header-Dateien). Überdenken Sie stattdessen Ihren Lösungsansatz, falls dieser nur mit einer Änderung an einer anderen Stelle im Code funktioniert.

Hilfestellung

Klicken Sie auf die folgenden Tipps, um einige Ratschläge zur Implementierung der jeweiligen Funktion zu erhalten!

Implementierung der vote-Funktion
  • Die Funktion nimmt drei Argumente entgegen: voter, rank und name.
  • Wenn name mit dem Namen eines gültigen Kandidaten übereinstimmt, muss das globale Präferenzen-Array aktualisiert werden, um zu speichern, dass der Wähler voter diesen Kandidaten als seine rank-Präferenz hat. Denken Sie daran, dass 0 die erste Präferenz ist, 1 die zweite Präferenz, usw. Sie können davon ausgehen, dass keine zwei Kandidaten den gleichen Namen haben.
  • Wenn die Präferenz erfolgreich gespeichert wurde, sollte die Funktion true zurückgeben. Andernfalls sollte die Funktion false zurückgeben. Dies sollte beispielsweise der Fall sein, wenn name nicht der Name eines der Kandidaten ist.

Beachten Sie beim Schreiben Ihres Codes zudem folgende Hinweise:

  • Erinnern Sie sich, dass candidate_count die Gesamtzahl der Kandidaten für die Wahl speichert.
  • Erinnern Sie sich daran, dass Sie strcmp verwenden können, um zwei Zeichenketten zu vergleichen.
  • Erinnern Sie sich, dass preferences[i][j] den Index des Kandidaten speichert, der die j-te Präferenz des i-ten Wählers ist.
Implementierung der tabulate Funktion
  • Die Funktion sollte die Anzahl der votes aktualisieren, die jeder Kandidat in dieser Phase der Stichwahl hat.
  • In jeder Phase der Stichwahl stimmt jeder Wähler für den von ihm bevorzugten Kandidaten, der noch nicht ausgeschieden ist.

Beachten Sie beim Schreiben Ihres Codes zudem folgende Hinweise:

  • Erinnern Sie sich, dass voter_count die Gesamtzahl der Wähler in der Wahl speichert und dass wir für jeden Wähler in unserer Wahl einen Stimmzettel auswerten müssen.
  • Erinnern Sie sich daran, dass für einen Wähler i der Kandidat seiner ersten Wahl durch preferences[i][0] repräsentiert wird, der Kandidat seiner zweiten Wahl durch preferences[i][1], usw.
  • Erinnern Sie sich daran, dass die candidate struct einen Member namens eliminated hat, das true ist, wenn der Kandidat von der Wahl ausgeschlossen wurde.
  • Erinnern Sie sich, dass die candidate struct einen Member namens votes hat, das Sie jeweils für den bevorzugten Kandidaten jedes Wählers aktualisieren müssen.
  • Denken Sie daran, dass sobald eine Stimme für den ersten nicht ausgeschlossenen Kandidaten eines Wählers gezählt wurde, die Auswertung des Stimmzettels dieses Wählers in diesem Durchgang beendet werden sollte. Sie können eine Schleife vorzeitig verlassen, indem Sie break innerhalb einer Bedingung verwenden.
Implementierung der print_winner Funktion
  • Wenn ein Kandidat mehr als die Hälfte der Stimmen hat, sollte sein Name ausgegeben werden und die Funktion sollte true zurückgeben.
  • Wenn noch niemand die Wahl gewonnen hat, sollte die Funktion false zurückgeben.

Beachten Sie beim Schreiben Ihres Codes diesen Hinweis:

  • Erinnern Sie sich, dass voter_count die Gesamtzahl der Wähler in der Wahl speichert. Wie kann man also die Anzahl der Stimmen berechnen, die man braucht, um die Wahl zu gewinnen?
Implementierung der find_min Funktion
  • Die Funktion sollte die geringste Stimmenzahl unter allen Kandidaten zurückgeben, die noch nicht von der Wahl ausgeschlossen wurden.

Beachten Sie beim Schreiben Ihres Codes diesen Hinweis:

  • Sie werden wahrscheinlich über alle Kandidaten iterieren, um denjenigen zu finden, der noch im Rennen ist und die wenigsten Stimmen hat. Welche Inhalte der candidate-struct sind demnach von Bedeutung?
Implementierung der is_tie Funktion
  • Die Funktion nimmt ein Argument min entgegen, das die aktuell geringste Anzahl an Stimmen unter allen Kandidaten der Wahl angibt.
  • Die Funktion sollte true zurückgeben, wenn jeder Kandidat, der noch im Rennen ist, die gleiche Anzahl an Stimmen hat, und sollte andernfalls false zurückgeben.

Beachten Sie beim Schreiben Ihres Codes diesen Hinweis:

  • Erinnern Sie sich daran, dass ein Unentschieden vorliegt, wenn jeder Kandidat, der noch in der Wahl ist, die gleiche Anzahl von Stimmen hat. Beachten Sie auch, dass die Funktion is_tie ein Argument min entgegennimmt, das die geringste Anzahl von Stimmen repräsentiert, die ein Kandidat derzeit hat. Wie könnten Sie min verwenden, um festzustellen, ob die Wahl unentschieden ist (oder umgekehrt, ob sie nicht unentschieden ist)?
Implementierung der eliminate Funktion
  • Die Funktion nimmt ein Argument min entgegen, das die geringste Anzahl an Stimmen unter allen noch verbleibenden Kandidaten der Wahl repräsentiert.
  • Die Funktion sollte alle Kandidaten als ausgeschlossen kennzeichnen, deren Anzahl an Stimmen dem Wert von min entspricht.

Testen

Stellen Sie beim Testen Ihres Codes sicher, dass dieser mit folgenden Szenarien umgehen kann:

  • einer Wahl mit einer beliebigen Anzahl von Kandidaten (bis zum MAX von 9)
  • der Stimmabgabe für einen Kandidaten nach Namen
  • ungültigen Stimmen für Kandidaten, die nicht auf dem Stimmzettel stehen (das Programm sollte sich in diesem Fall beenden)
  • der Ausgabe des Gewinners der Wahl, wenn es nur einen gibt
  • einer Stimmengleichheit zwischen allen verbleibenden Kandidaten, so dass niemand ausgeschlossen wird und alle verbleibenden Kandidaten als Sieger ausgegeben werden

Korrektheit

Führen Sie in Ihrem Terminal den folgenden Befehl aus, um die Korrektheit Ihrer Arbeit zu überprüfen:

check50 -l cs50/problems/2024/x/runoff

Style

Führen Sie den folgenden Befehl aus, um den Stil Ihres Codes mit style50 zu analysieren:

style50 runoff.c