Autor Beitrag
Narses
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Administrator
Beiträge: 10181
Erhaltene Danke: 1254

W10ent
TP3 .. D7pro .. D10.2CE
BeitragVerfasst: Di 25.12.07 02:06 
Moin!

Hier nun eine (Muster-)Lösung für Paranuss 1: Karte färben. ;) Da die Durchsicht der Abgaben etwas dauern wird, möchte ich an dieser Stelle schonmal um etwas Geduld bitten. :?

Wie wir im Team beim Löschen der Tipps aus den Beiträgen im laufenden Gewinnspiel schon gesehen haben :P sind bereits Einige dahinter gekommen, dass das Grundproblem aus der Graphentheorie stammt, konkret geht es um die Färbung eines Graphen. Um das ganze etwas anschaulicher zu gestalten, haben wir uns auf eine Teilmenge der Graphen beschränkt, die planaren Graphen, da man diese zum einen schön als Landkarte darstellen kann und zum anderen für diese Graphen-Klasse ein Spezialfall gilt: der 4-Farben-Satz:
Wikipedia hat folgendes geschrieben:
Der Vier-Farben-Satz [...] besagt, dass vier Farben immer ausreichen, um eine beliebige Landkarte in der euklidischen Ebene so einzufärben, dass keine zwei angrenzenden Länder die gleiche Farbe bekommen. [...] Dies gilt unter den Einschränkungen, dass ein gemeinsamer Punkt nicht als "Grenze" zählt und jedes Land aus einer zusammenhängenden Fläche besteht, also keine Exklaven vorhanden sind.
Daraus folgt speziell für diese Paranuss:
Wenn 4 Farben nicht ausreichen, um die Karte zu färben, ist der Graph entweder nicht planar oder der Algorithmus zum Bestimmen der Färbung ist nicht korrekt!
Da wir den Graphen aus einem Kartenbild ableiten, muss er planar (gewesen) sein. Daraus folgt: wenn man mehr als 4 Farben benötigt, ist der gewählte Algorithmus zur Bestimmung der Kanten des Graphen (=angrenzende Flächen) nicht korrekt. Fazit: Bei 5 Farben kann die Lösung nicht korrekt sein.

Die Aufgabe lässt sich in drei Teilprobleme zerlegen:
  1. Wählen einer geeigneten Datenstruktur für die interne Abbildung eines planaren Graphen
  2. Bestimmen des Graphen aus einem Landkartenbild
  3. Bestimmen einer gültigen Färbung (mit max. 4 Farben)
Wir werden einen OOP-Ansatz verfolgen. Hier zunächst die (leicht gekürzte) Deklaration der Graphenklasse:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
type
  // eine Fläche des Graphen
  TPlane = class(TObject)
  private
    FOwner: TPlanarGraph;
    //...
  public
    constructor Create(AOwner: TPlanarGraph; const AID,X,Y: Integer);
    destructor Destroy; override;
    property ID: Integer read FID; // ID der Fläche (=Index)
    property Point: TPoint read FPoint; // Koordinaten eines Punktes in der Fläche
    property Color: TColor read FColor write SetColor; // aktuelle Flächenfarbe (GUI)
    property ColorIndex: Integer read FColorIndex write FColorIndex; // Farbindex (für korrekte Färbung)
    procedure SetColorByIndex; // Farbe aus ColorIndex ableiten und setzen
    property Connections: TList read FConnections; // Liste der angrenzenden Flächen
    function IsColorIndexValid: Boolean; // haben angrenzende Flächen andere Farben?
  end;

  // der planare Graph als Flächenliste
  TPlanarGraph = class(TObjectList)
  private
    FImageRef: TImage;  // GUI-Image-Komponente, die die Kartengrafik enthält
    FPlaneMap: TBitmap; // interne Kartengrafik zur Flächenidentifikation
  public
    constructor Create;
    destructor Destroy; override;
    procedure Pickup(AImage: TImage); // Graph aus einer Kartengrafik ableiten und färben
    property Plane[const AIndex: Integer]: TPlane read GetPlane;
    function GetPlaneByPoint(Point: TPoint): TPlane;
    procedure SetColorByIndex; // alle Flächen passend zum ColorIndex färben (GUI)
  end;

implementation

// -----------------------------------------------------------------------------
// TPlane

// eine neue Fläche anlegen
constructor TPlane.Create(AOwner: TPlanarGraph; const AID,X,Y: Integer);
begin
  inherited Create;
  FConnections := TList.Create;
  FOwner := AOwner; // Eigentümer merken, wird für GUI-Zugriff und Flächenidentifikation benötigt
  FID := AID;
  FPoint.X := X;
  FPoint.Y := Y;
  FColorIndex := -1;
  with FOwner.FPlaneMap.Canvas do begin // Fläche mit der ID als Farbe markieren
    Brush.Color := FID; // Fläche mit dieser Farbe (=ID)...
    FloodFill(FPoint.X,FPoint.Y,Pixels[FPoint.X,FPoint.Y],fsSurface); // ...ausmalen
  end;
end;

// entsprechende Fläche im GUI-Image färben
procedure TPlane.SetColor(const Value: TColor);
begin
  if (FColor <> Value) then begin // notwendig?
    FColor := Value;
    with FOwner.FImageRef.Picture.Bitmap.Canvas do begin // GUI-Image-Referenz aus der Graphenklasse
      Brush.Color := FColor;
      FloodFill(FPoint.X,FPoint.Y,Pixels[FPoint.X,FPoint.Y],fsSurface);
    end;
  end;
end;

// ColorIndex in eine Farbe aus der Default-Farbtabelle umsetzen
procedure TPlane.SetColorByIndex;
begin
  SetColor(GraphColor[2,FColorIndex]);
end;

// herausfinden, ob die angrenzenden Flächen einen anderen ColorIndex haben
function TPlane.IsColorIndexValid: Boolean;
  var
    i: Integer;
begin
  Result := FALSE; // Default: ColorIndex kommt in der Umgebung vor
  for i := 0 to FConnections.Count-1 do // alle angrenzenden Flächen absuchen
    if (TPlane(FConnections.Items[i]).ColorIndex = FColorIndex) then // gleich dem eigenen?
      Exit; // ja, dann raus hier
  Result := TRUE; // alle angrenzenden Flächen sind anders gefärbt
end;

// -----------------------------------------------------------------------------
// TPlanarGraph

constructor TPlanarGraph.Create;
begin
  inherited Create;

  FPlaneMap := TBitmap.Create;
end;

// Wrapper-Array-Eigenschaft, um die Typecasts zu vermeiden
function TPlanarGraph.GetPlane(const AIndex: Integer): TPlane;
begin
  Result := TPlane(Items[AIndex]);
end;

// Fläche über die Koordinaten eines Punktes bestimmen (oder NIL liefern)
function TPlanarGraph.GetPlaneByPoint(Point: TPoint): TPlane;
  var
    PlaneID: Integer;
begin
  Result := NIL// Default: an diesem Punkt ist keine Fläche
  if (Count > 0then begin // überhaupt Flächen da?
    PlaneID := FPlaneMap.Canvas.Pixels[Point.X,Point.Y]; // Farbe ist Flächen-ID
    if (PlaneID <= Count) then // Randpixel haben $FEFEFE als Farbe
      Result := GetPlane(PlaneID); // Fläche über Index identifiziert
  end;
end;

Das Kernstück ist natürlich die Methode TPlanarGraph.Pickup(AImage: TImage), die wir jetzt im Detail untersuchen wollen:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
procedure TPlanarGraph.Pickup(AImage: TImage);
  var
    x,y: Integer;
    ScanLine: PByteArray;
    Plane: TPlane;
    TempPlanes: TList;

  procedure ExaminePixelEnvironment(const BaseX, BaseY: Integer); // siehe unten

  function Colorize(const APlaneID: Integer): Boolean; // siehe unten

begin
  Self.Clear; // alte Flächen entsorgen
  if Assigned(AImage) then begin // das GUI-Image muss gültig sein
    AImage.Picture.Bitmap.PixelFormat := pf24bit; // Pixelformat erzwingen
    FImageRef := AImage; // Referenz merken
    FPlaneMap.Assign(AImage.Picture.Bitmap); // Kopie anfertigen -> interner Gebrauch
    for y := 0 to FPlaneMap.Height-1 do begin // alle Zeilen der Kopie durchgehen
      ScanLine := FPlaneMap.ScanLine[y]; // Zeilenreferenz wg. Zugriffsgeschwindigkeit
      for x := 0 to 3*FPlaneMap.Width-1 do // alle Pixel normieren: weiß->weiß, schwarz->$FEFEFE
        Scanline^[x] := 254 or (Scanline^[x] and 1); // 255->255; 0->254
    end;
    // =A=Flächen=finden=====================================================================
    for y := 1 to FPlaneMap.Height-2 do // alle Zeilen
      for x := 1 to FPlaneMap.Width-2 do // und alle Spalten
        if (FPlaneMap.Canvas.Pixels[x,y] = clWhite) then // nach weißen Pixeln absuchen
          Add(TPlane.Create(Self,Self.Count,x,y)); // neue Fläche vermerken
    // ======================================================================================
    if (Self.Count > 0then begin // sind überhaupt Flächen da?
      TempPlanes := TList.Create;
      try
        // =B=Flächenverbindungen=bestimmen======================================================
        for y := 1 to FPlaneMap.Height-2 do // Randlinien-Pixel und die 8 umliegenden untersuchen
          for x := 1 to FPlaneMap.Width-2 do
            if (FPlaneMap.Canvas.Pixels[x,y] = $FEFEFEthen // Rand gefunden
              ExaminePixelEnvironment(x,y); // 8 umliegende Pixel untersuchen
        // =C=Färbung=bestimmen==================================================================
        if NOT Colorize(0then
          raise Exception.Create('Der Graph ist nicht planar!');
        // ======================================================================================
      finally
        TempPlanes.Free;
      end;
    end;
  end
  else
    raise Exception.Create('Referenz-Image ungültig!');
end;

Zunächst wird eine Kopie der Kartengrafik angelegt und so bearbeitet, dass weiße Pixel unverändert bleiben, aber schwarze (=Rand) zu $FEFEFE umgefärbt werden. So können wir die gefundenen Flächen mit dem Index als Farbe markieren und das Bild später zum Identifizieren von Flächen verwenden.

A - Flächenerkennung
  • Pixelweise das Bild absuchen
  • Weißer Pixel gefunden -> hier ist eine neue Fläche -> Flächenobjekt in die Liste einfügen, beim Anlegen wird die Fläche über den Referenzpixel mit dem Index als Farbe gefärbt (per FloodFill)
  • Die Pixel einer bereits bekannten Fläche sind jetzt nicht mehr weiß und werden deshalb nicht mehr beachtet
B - Angrenzende Flächen bestimmen

Idee: das Bild nach Randpixeln absuchen und die Umgebung dieser Punkte auswerten. Dazu wird die folgende lokale Prozedur verwendet:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
  procedure ExaminePixelEnvironment(const BaseX, BaseY: Integer);
    var
      i,c: Integer;
  begin
    TempPlanes.Clear;
    with FPlaneMap.Canvas do
      for i := 0 to 8 do // umliegende 8 Pixel incl. Mitte = 9 Pixel
        if (i <> 4then begin // Mitte auslassen: 8 Pixel
          c := Pixels[BaseX+(i div 3)-1,BaseY+(i mod 3)-1]; // Farbe des Punktes
          if (c <> $FEFEFEthen begin // Rand ignorieren
            Plane := GetPlane(c); // die Flächen sind mit der ID als Farbe eingefärbt
            if (TempPlanes.IndexOf(Plane) = -1then // Verbindung ist neu
              TempPlanes.Add(Plane); // merken
          end;
        end;
    if (TempPlanes.Count = 2then // nur Umgebungen mit 2 Verbindung akzeptieren
      for i := 0 to 1 do begin
        Plane := TPlane(TempPlanes.Items[i]);
        if (Plane.Connections.IndexOf(TempPlanes.Items[1-i]) = -1then // neu?
          Plane.Connections.Add(TempPlanes.Items[1-i]); // ja, merken
      end;
  end;

Der "Trick" ist hierbei, nur Pixelumgebungen mit genau zwei verschiedenen Flächen zu beachten. Zusammen mit der Kartenbildvorgabe, dass die kleinste Fläche ein 3-Pixel großes Dreieck ist, ergibt sich so immer ein planarer Graph. ;)

C - Färbung bestimmen

Die Färbung bekommen wir über einen simplen Backtracking-Algorithmus, der zwar im schlechtesten Fall alle Kombinationen ausprobiert, aber dafür einen planaren Graphen garantiert mit 4 Farben färbt. Geht das nicht, kann der Graph nicht planar sein! :idea:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
  function Colorize(const APlaneID: Integer): Boolean;
    var
      i: Integer;
  begin
    Result := (APlaneID < 0or (APlaneID >= Count); // gültiger Flächenindex?
    if (NOT Result) then begin // ja, die Fläche färben
      for i := 0 to 3 do begin // jede Fläche hat 4 Freiheitsgrade
        GetPlane(APlaneID).ColorIndex := i; // aktuelle Farbe setzen und...
        if GetPlane(APlaneID).IsColorIndexValid then begin // ...prüfen, ob sie "passt"
          Result := Colorize(APlaneID +1); // bisher OK, nächste Fläche färben (rekursiv)
          if Result then // die untergeordnete Farbwahl ist OK...
            Exit; // ...dann sind wir hier fertig!
          // wenn wir hier ankommen, war die aktuelle Farbe "nicht gut"
        end;
        // nächste Farbe probieren
      end;
      // wenn wir hier ankommen, hat keine der 4 Farben gepasst, also...
      GetPlane(APlaneID).ColorIndex := -1// Fläche wieder auf "ungefärbt" setzen und...
    end// ...in der nächst höheren Rekursionsebene die nächste Farbe probieren
  end;

Der Funktionswert sagt aus, ob die angegebene Fläche (regelgerecht) gefärbt werden konnte. Der erste Aufruf erfolgt deshalb mit der ersten Fläche (Index 0). Wird hier FALSE zurückgeliefert, dann konnte keine Färbung für den ganzen Graphen bestimmt werden. In diesem Fall erheben wir einfach mal eine Ausnahme, denn das sollte eigentlich nicht auftreten können. ;)

Das komplette Musterprojekt befindet sich mit Quelltext und Testbildern im Anhang.

cu
Narses
Einloggen, um Attachments anzusehen!
_________________
There are 10 types of people - those who understand binary and those who don´t.
Martok
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Di 25.12.07 03:49 
Sehr schön. Ich habs richtig, glaub ich ;)
EDIT: Mist, doch nicht. Zwei Testbilder sind, sonst wärs nicht mal wem aufgefallen... Ich glaub aber, ich hätte nen Fix dafür :roll:

Deine Flächenerkennung gefällt mir. Schön einfach, da wär ich so nicht drauf gekommen.

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."
Xion
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
EE-Maler
Beiträge: 1952
Erhaltene Danke: 128

Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
BeitragVerfasst: Di 25.12.07 14:52 
:eyes: voll auf die Schwachstellen von meinem Programm rumhacken :lol: gemeine Karten...naja, bin mal gespannt, bei wem das so funktioniert.

Vor allem frag ich mich, warum die letzte Karte nicht geht *grübel*

_________________
a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
Martok
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Di 25.12.07 15:19 
Die Letzte wurde sogar mal als Gegenbeweis für den 4-Farben-Satz angebracht. Bis dann einer bewiesen hat, dass sie auch 4-Färbbar ist... hatte ich irgendwo bei meinen Nachforschungen gelesen...

Ich hatte halt zuviel Vertrauen in mein Programm: statt zu Backtracken, dachte ich die erste Lösung wäre schon optimal :?

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Di 25.12.07 17:54 
user profile iconMartok hat folgendes geschrieben:
Ich hatte halt zuviel Vertrauen in mein Programm: statt zu Backtracken, dachte ich die erste Lösung wäre schon optimal :?
Ich wusste, dass meine Lösung per Greedy-Algorithmus nicht immer funktioniert, ich bin aber schlicht nicht dazu gekommen das noch zu ändern, auch wenn es eigentlich gar nicht so viel gewesen wäre. Das habe ich auch in den Quelltext geschrieben, dass das nicht immer geht.
Morgen werde ich aber wohl dazu kommen und dann die jetzige Lösung und die korrigierte Variante vorstellen.

Alle Bilder bis auf das letzte Bild funktionieren bei mir.
Kha
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 3803
Erhaltene Danke: 176

Arch Linux
Python, C, C++ (vim)
BeitragVerfasst: Di 25.12.07 18:34 
Omg... tut euch einen Gefallen und streicht mein Programm im Voraus von der Liste. War doch keine so gute Idee, gestern in allerletzter Sekunde abzugeben und dabei fatalerweise folgende Zeile zu vergessen :autsch: :
user profile iconNarses hat folgendes geschrieben:
ausblenden Delphi-Quelltext
1:
2:
      // wenn wir hier ankommen, hat keine der 4 Farben gepasst, also...
      GetPlane(APlaneID).ColorIndex := -1// Fläche wieder auf "ungefärbt" setzen und...

Bei den Beispielen ist der Fehler nicht aufgefallen, weil dort Backtracking anscheinend nicht/wenig nötig ist; nur bei 111 hakt es (mit Fix in ca. 50 Sekunden, wie sieht's bei den anderen aus?).

PS: 000.bmp ist gemein :P .
GTA-Place
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
EE-Regisseur
Beiträge: 5248
Erhaltene Danke: 2

WIN XP, IE 7, FF 2.0
Delphi 7, Lazarus
BeitragVerfasst: Di 25.12.07 20:46 
Hab grad ne PN bekommen, dass mein Programm 111_von_Fiete.bmp mit 5 Farben statts 4 färbt. Kann ich im Moment aber nicht ausprobieren. Schade.

_________________
"Wer Ego-Shooter Killerspiele nennt, muss konsequenterweise jeden Horrorstreifen als Killerfilm bezeichnen." (Zeit.de)
Chryzler
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 1097
Erhaltene Danke: 2



BeitragVerfasst: Di 25.12.07 22:06 
Mein Prog ist auch fehlerhaft. :( Bin mal gespannt wie viele das richtig hinbekommen haben. :?
Xion
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
EE-Maler
Beiträge: 1952
Erhaltene Danke: 128

Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
BeitragVerfasst: Mi 26.12.07 11:07 
ich glaub das mit der Karte war wohl die schwerste Paranuss...Schade, dass ich nicht mehr Zeit dafür hatte. Naja, mitmachen ist alles :D

_________________
a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)