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?
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:
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:
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
undname
. - Wenn
name
mit dem Namen eines gültigen Kandidaten übereinstimmt, muss das globale Präferenzen-Array aktualisiert werden, um zu speichern, dass der Wählervoter
diesen Kandidaten als seinerank
-Präferenz hat. Denken Sie daran, dass0
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 Funktionfalse
zurückgeben. Dies sollte beispielsweise der Fall sein, wennname
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 diej
-te Präferenz desi
-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 durchpreferences[i][0]
repräsentiert wird, der Kandidat seiner zweiten Wahl durchpreferences[i][1]
, usw. - Erinnern Sie sich daran, dass die
candidate
struct
einen Member namenseliminated
hat, dastrue
ist, wenn der Kandidat von der Wahl ausgeschlossen wurde. - Erinnern Sie sich, dass die
candidate
struct
einen Member namensvotes
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 andernfallsfalse
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 Argumentmin
entgegennimmt, das die geringste Anzahl von Stimmen repräsentiert, die ein Kandidat derzeit hat. Wie könnten Siemin
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
von9
) - 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