Проста візуалізація графа

Дерева та Графи

Теоретичні основи теорії графів

Граф — це математична структура, що складається з множини вершин (вузлів) та множини ребер (зв'язків), які з'єднують пари вершин. Графи застосовуються для моделювання зв'язків у різних системах: соціальних мережах, транспортних маршрутах, комп'ютерних мережах, базах даних тощо.

Основні визначення

Класифікація графів

Приклади графів

На рисунку нижче зображено приклад неорієнтованого графа.

Приклад неорієнтованого графа

Граф називається зваженим, якщо кожному ребру відповідає певне число (вага). Наприклад, у транспортній мережі вага може означати відстань або час.

Способи задання графів

Існує кілька основних способів представлення графа в пам'яті комп'ютера:

  1. Матриця суміжності — квадратна матриця розміром n × n, де n — кількість вершин. Елемент aij = 1, якщо існує ребро між вершинами i та j, інакше 0. Для зважених графів замість 1 зберігається вага ребра.
  2. Список суміжності — для кожної вершини зберігається список суміжних з нею вершин.
  3. Список ребер — масив пар (або трійок для зважених) вершин.
Матриця суміжності — це матриця A розміру n × n, де Aij = 1, якщо існує ребро (i, j), і 0 в іншому випадку.

Розглянемо граф, зображений на рисунку:

Граф для прикладу матриці суміжності

Матриця суміжності для цього графа:

ABCDE
A01001
B10111
C01010
D01101
E11010

Матриця суміжності неорієнтованого графа завжди симетрична відносно головної діагоналі.

Властивості графів

  1. Сума степенів усіх вершин графа дорівнює подвоєній кількості ребер.
  2. У будь-якому графі кількість вершин непарного степеня парна.
  3. У будь-якому графі з n ≥ 2 існують принаймні дві вершини з однаковим степенем.
  4. Максимальна кількість ребер у простому графі з n вершинами дорівнює n(n−1)/2.

Ейлерові та гамільтонові шляхи

У 1736 році Леонард Ейлер розв'язав задачу про кенігсберзькі мости, що поклало початок теорії графів.

Схема кенігсберзьких мостів

Задача про сім мостів Кенігсберга: чи можна пройти по всіх семи мостах, не проходячи жодного двічі? Ейлер довів, що це неможливо, оскільки відповідний граф має чотири вершини непарного степеня.

Алгоритми на графах (короткий огляд)

Проєкт «Найпростіша візуалізація графа»

У цьому проєкті ми створимо програму на C# (Windows Forms), яка дозволяє:

Для роботи використовується компонент PictureBox для малювання, DataGridView для відображення списку вершин і матриці суміжності.

Крок 1. Створення інтерфейсу

Додайте на форму:

  1. PictureBox (назва pictureBox1) — для малювання графа.
  2. DataGridView для списку вершин (dataGridView1).
  3. DataGridView для матриці суміжності (dgw).
  4. Елемент MenuStrip знаходиться на панелі елементів у групі Меню. Елемент призначений для формування головного меню.MenuStrip (назва menuStrip1)
  5. Додамо на форму елемент OpenDialog.
Інтерфейс програмиІнтерфейс програми Інтерфейс програмиІнтерфейс програми

Повний код класу Form1.Designer


namespace GraphVisualization
{
    partial class Form1
    {
        /// 
        /// Required designer variable.
        /// 
        private System.ComponentModel.IContainer components = null;

        /// 
        /// Clean up any resources being used.
        /// 
        /// true if managed resources should be disposed; otherwise, false.
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// 
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// 
        private void InitializeComponent()
        {
            this.menuStrip1 = new System.Windows.Forms.MenuStrip();
            this.graphToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.newGraphToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.clearToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.mapsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.matrixToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.generateMatrixToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.toSpanningToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
            this.pictureBox = new System.Windows.Forms.PictureBox();
            this.dataGridViewPoints = new System.Windows.Forms.DataGridView();
            this.dataGridViewMatrix = new System.Windows.Forms.DataGridView();
            this.labelPoints = new System.Windows.Forms.Label();
            this.labelMatrix = new System.Windows.Forms.Label();
            this.openFileDialog = new System.Windows.Forms.OpenFileDialog();
            this.menuStrip1.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridViewPoints)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridViewMatrix)).BeginInit();
            this.SuspendLayout();
            // 
            // menuStrip1
            // 
            this.menuStrip1.GripMargin = new System.Windows.Forms.Padding(2, 2, 0, 2);
            this.menuStrip1.ImageScalingSize = new System.Drawing.Size(24, 24);
            this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.graphToolStripMenuItem,
            this.matrixToolStripMenuItem,
            this.exitToolStripMenuItem});
            this.menuStrip1.Location = new System.Drawing.Point(0, 0);
            this.menuStrip1.Name = "menuStrip1";
            this.menuStrip1.Size = new System.Drawing.Size(1924, 36);
            this.menuStrip1.TabIndex = 0;
            this.menuStrip1.Text = "menuStrip1";
            // 
            // graphToolStripMenuItem
            // 
            this.graphToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.newGraphToolStripMenuItem,
            this.clearToolStripMenuItem,
            this.mapsToolStripMenuItem});
            this.graphToolStripMenuItem.Name = "graphToolStripMenuItem";
            this.graphToolStripMenuItem.Size = new System.Drawing.Size(76, 32);
            this.graphToolStripMenuItem.Text = "Graph";
            // 
            // newGraphToolStripMenuItem
            // 
            this.newGraphToolStripMenuItem.Name = "newGraphToolStripMenuItem";
            this.newGraphToolStripMenuItem.Size = new System.Drawing.Size(184, 34);
            this.newGraphToolStripMenuItem.Text = "Generate";
            this.newGraphToolStripMenuItem.Click += new System.EventHandler(this.newGraphToolStripMenuItem_Click);
            // 
            // clearToolStripMenuItem
            // 
            this.clearToolStripMenuItem.Name = "clearToolStripMenuItem";
            this.clearToolStripMenuItem.Size = new System.Drawing.Size(184, 34);
            this.clearToolStripMenuItem.Text = "Clear";
            this.clearToolStripMenuItem.Click += new System.EventHandler(this.clearToolStripMenuItem_Click);
            // 
            // mapsToolStripMenuItem
            // 
            this.mapsToolStripMenuItem.Name = "mapsToolStripMenuItem";
            this.mapsToolStripMenuItem.Size = new System.Drawing.Size(184, 34);
            this.mapsToolStripMenuItem.Text = "Maps";
            this.mapsToolStripMenuItem.Click += new System.EventHandler(this.mapsToolStripMenuItem_Click);
            // 
            // matrixToolStripMenuItem
            // 
            this.matrixToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.generateMatrixToolStripMenuItem,
            this.toSpanningToolStripMenuItem});
            this.matrixToolStripMenuItem.Name = "matrixToolStripMenuItem";
            this.matrixToolStripMenuItem.Size = new System.Drawing.Size(77, 32);
            this.matrixToolStripMenuItem.Text = "Matrix";
            // 
            // generateMatrixToolStripMenuItem
            // 
            this.generateMatrixToolStripMenuItem.Name = "generateMatrixToolStripMenuItem";
            this.generateMatrixToolStripMenuItem.Size = new System.Drawing.Size(184, 34);
            this.generateMatrixToolStripMenuItem.Text = "Generate";
            this.generateMatrixToolStripMenuItem.Click += new System.EventHandler(this.generateMatrixToolStripMenuItem_Click);
            // 
            // toSpanningToolStripMenuItem
            // 
            this.toSpanningToolStripMenuItem.Name = "toSpanningToolStripMenuItem";
            this.toSpanningToolStripMenuItem.Size = new System.Drawing.Size(184, 34);
            // 
            // exitToolStripMenuItem
            // 
            this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
            this.exitToolStripMenuItem.Size = new System.Drawing.Size(55, 32);
            this.exitToolStripMenuItem.Text = "Exit";
            this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
            // 
            // pictureBox
            // 
            this.pictureBox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
            this.pictureBox.Location = new System.Drawing.Point(18, 42);
            this.pictureBox.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
            this.pictureBox.Name = "pictureBox";
            this.pictureBox.Size = new System.Drawing.Size(702, 614);
            this.pictureBox.TabIndex = 1;
            this.pictureBox.TabStop = false;
            this.pictureBox.Click += new System.EventHandler(this.pictureBox_Click);
            // 
            // dataGridViewPoints
            // 
            this.dataGridViewPoints.AllowUserToAddRows = false;
            this.dataGridViewPoints.AllowUserToDeleteRows = false;
            this.dataGridViewPoints.AllowUserToResizeColumns = false;
            this.dataGridViewPoints.AllowUserToResizeRows = false;
            this.dataGridViewPoints.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
            this.dataGridViewPoints.Enabled = false;
            this.dataGridViewPoints.Location = new System.Drawing.Point(728, 65);
            this.dataGridViewPoints.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
            this.dataGridViewPoints.Name = "dataGridViewPoints";
            this.dataGridViewPoints.RowHeadersWidth = 62;
            this.dataGridViewPoints.Size = new System.Drawing.Size(240, 591);
            this.dataGridViewPoints.TabIndex = 2;
            // 
            // dataGridViewMatrix
            // 
            this.dataGridViewMatrix.AllowUserToAddRows = false;
            this.dataGridViewMatrix.AllowUserToDeleteRows = false;
            this.dataGridViewMatrix.AllowUserToResizeColumns = false;
            this.dataGridViewMatrix.AllowUserToResizeRows = false;
            this.dataGridViewMatrix.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
            this.dataGridViewMatrix.Location = new System.Drawing.Point(976, 66);
            this.dataGridViewMatrix.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
            this.dataGridViewMatrix.Name = "dataGridViewMatrix";
            this.dataGridViewMatrix.RowHeadersWidth = 62;
            this.dataGridViewMatrix.Size = new System.Drawing.Size(700, 591);
            this.dataGridViewMatrix.TabIndex = 3;
            // 
            // labelPoints
            // 
            this.labelPoints.AutoSize = true;
            this.labelPoints.Location = new System.Drawing.Point(827, 42);
            this.labelPoints.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
            this.labelPoints.Name = "labelPoints";
            this.labelPoints.Size = new System.Drawing.Size(53, 20);
            this.labelPoints.TabIndex = 4;
            this.labelPoints.Text = "Points";
            // 
            // labelMatrix
            // 
            this.labelMatrix.AutoSize = true;
            this.labelMatrix.Location = new System.Drawing.Point(1226, 41);
            this.labelMatrix.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
            this.labelMatrix.Name = "labelMatrix";
            this.labelMatrix.Size = new System.Drawing.Size(128, 20);
            this.labelMatrix.TabIndex = 5;
            this.labelMatrix.Text = "Adjacency matrix";
            // 
            // openFileDialog
            // 
            this.openFileDialog.FileName = "openFileDialog1";
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.AutoScroll = true;
            this.AutoSize = true;
            this.ClientSize = new System.Drawing.Size(1924, 675);
            this.Controls.Add(this.labelMatrix);
            this.Controls.Add(this.labelPoints);
            this.Controls.Add(this.dataGridViewMatrix);
            this.Controls.Add(this.dataGridViewPoints);
            this.Controls.Add(this.pictureBox);
            this.Controls.Add(this.menuStrip1);
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
            this.MainMenuStrip = this.menuStrip1;
            this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
            this.Name = "Form1";
            this.Text = "GraphVisualization";
            this.menuStrip1.ResumeLayout(false);
            this.menuStrip1.PerformLayout();
            ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridViewPoints)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridViewMatrix)).EndInit();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.MenuStrip menuStrip1;
        private System.Windows.Forms.ToolStripMenuItem graphToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem newGraphToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem clearToolStripMenuItem;
        private System.Windows.Forms.PictureBox pictureBox;
        private System.Windows.Forms.DataGridView dataGridViewPoints;
        private System.Windows.Forms.DataGridView dataGridViewMatrix;
        private System.Windows.Forms.Label labelPoints;
        private System.Windows.Forms.Label labelMatrix;
        private System.Windows.Forms.ToolStripMenuItem matrixToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem generateMatrixToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem toSpanningToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
        private System.Windows.Forms.ToolStripMenuItem mapsToolStripMenuItem;
        private System.Windows.Forms.OpenFileDialog openFileDialog;
    }
}

Крок 2. Повний код класу Form1

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace GraphVisualization
{
    // Структура, що представляє ребро графа: дві вершини та вагу (довжину)
    struct Edge
    {
        public MyPoint Point1;   // Перша вершина ребра
        public MyPoint Point2;   // Друга вершина ребра
        public int Weight;       // Вага ребра (відстань між вершинами)

        // Конструктор для створення ребра
        public Edge(MyPoint point1, MyPoint point2, int weight)
        {
            Point1 = point1;
            Point2 = point2;
            Weight = weight;
        }
    }

    // Структура, що представляє вершину графа: ідентифікатор та координати
    struct MyPoint
    {
        public int Id;   // Унікальний номер вершини
        public int X;    // Координата X на площині
        public int Y;    // Координата Y на площині

        // Конструктор для створення вершини
        public MyPoint(int id, int x, int y)
        {
            Id = id;
            X = x;
            Y = y;
        }
    }

    public partial class Form1 : Form
    {
        // Діалогове вікно для вибору файлу зображення
        OpenFileDialog _open_file = new OpenFileDialog();

        // Бітова карта для малювання графа (буфер)
        private Bitmap bitmap;

        // Список усіх вершин графа
        private List points = new List();

        // Список усіх ребер графа
        private List edges = new List();

        // Графічний контекст для малювання на bitmap
        private Graphics graphics;

        // Константи для налаштування зовнішнього вигляду
        private const int Radius = 6;                // Радіус кола вершини
        private const int EdgeThickness = 2;         // Товщина ліній ребер
        private const int PointLabelFontSize = 12;   // Розмір шрифту підпису вершини
        private const int EdgeLabelFontSize = 10;    // Розмір шрифту підпису ребра (ваги)
        private const string FontName = "Arial";     // Назва шрифту

        // Конструктор форми
        public Form1()
        {
            InitializeComponent();

            // Ініціалізація бітової карти розміром з PictureBox
            bitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
            // Створення графічного контексту для малювання на bitmap
            graphics = Graphics.FromImage(bitmap);

            // Налаштування DataGridView для відображення списку вершин
            dataGridViewPoints.Columns.Add("pointNumber", "№");           // Колонка з номером
            dataGridViewPoints.Columns.Add("pointСoordinates", "Point");  // Колонка з координатами
            dataGridViewPoints.Columns[0].Width = 30;   // Ширина колонки номера
            dataGridViewPoints.Columns[1].Width = 70;   // Ширина колонки координат

            // Налаштування DataGridView для матриці суміжності
            dataGridViewMatrix.RowHeadersWidth = 50;    // Ширина заголовків рядків
        }

        // Метод для відкриття діалогового вікна вибору зображення
        private string OpenImageFile()
        {
            string path = "";

            // Налаштування діалогового вікна
            _open_file.InitialDirectory = Directory.GetCurrentDirectory();
            _open_file.Filter = "Image Files(*.JPG;*.GIF;*.PNG)|*.JPG;*.GIF;*.PNG|All files (*.*)|*.*";
            _open_file.FilterIndex = 2;
            _open_file.RestoreDirectory = true;

            // Якщо користувач обрав файл, зберігаємо шлях
            if (_open_file.ShowDialog() == DialogResult.OK)
            {
                path = _open_file.FileName;
            }
            return path;
        }

        // Обчислення відстані між двома точками (теорема Піфагора)
        private int EdgeLength(MyPoint point1, MyPoint point2)
        {
            return (int) Math.Sqrt(Math.Pow(point1.X - point2.X, 2) + Math.Pow(point1.Y - point2.Y, 2));
        }

        // Малювання однієї вершини (коло та підпис)
        private void DrawPoint(MyPoint point, string text)
        {
            // Малюємо зафарбоване коло червоного кольору
            graphics.FillEllipse(new SolidBrush(Color.Red),
                point.X - Radius, point.Y - Radius, Radius * 2, Radius * 2);

            // Малюємо текст (номер вершини) біля кола
            graphics.DrawString(text, new Font(FontName, PointLabelFontSize),
                new SolidBrush(Color.Black), point.X + Radius, point.Y + Radius);

            // Оновлюємо зображення в PictureBox
            pictureBox.Image = bitmap;
        }

        // Заповнення матриці суміжності значеннями (для демонстрації)
        private void FillMatrix(int number)
        {
            for (int i = 0; i < points.Count; i++)
            {
                for (int j = 0; j < points.Count; j++)
                {
                    // Заповнюємо верхню трикутну матрицю числом, нижню - нулями
                    dataGridViewMatrix.Rows[i].Cells[j].Value = (i < j) ? number : 0;
                }
            }
        }

        // Генерація порожньої матриці суміжності відповідного розміру
        private void GenerateMatrix()
        {
            ClearMatrix(); // Спочатку очищаємо матрицю

            // Додаємо стовпці для кожної вершини
            foreach (var point in points)
            {
                DataGridViewColumn column = new DataGridViewTextBoxColumn();
                column.Name = $"{point.Id}";
                column.Width = 25;
                dataGridViewMatrix.Columns.Add(column);
            }

            // Додаємо рядки та встановлюємо заголовки
            foreach (var point in points)
            {
                dataGridViewMatrix.Rows.Add();
                dataGridViewMatrix.Rows[dataGridViewMatrix.Rows.Count - 1].HeaderCell.Value = $"{point.Id}";
            }

            // Заповнюємо матрицю одиницями у верхній трикутній частині
            FillMatrix(1);

            // Центруємо текст у комірках
            foreach (DataGridViewColumn column in dataGridViewMatrix.Columns)
            {
                column.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
            }
        }

        // Генерація списку ребер на основі матриці суміжності
        private void GenerateEdges()
        {
            edges.Clear(); // Очищаємо попередній список ребер

            for (int i = 0; i < points.Count; i++)
            {
                for (int j = 0; j < points.Count; j++)
                {
                    // Якщо в матриці стоїть 1 і це не петля (i != j)
                    if (i != j && Convert.ToInt32(dataGridViewMatrix.Rows[i].Cells[j].Value) == 1)
                    {
                        // Створюємо копії точок, щоб уникнути зміни оригіналів
                        MyPoint p1 = new MyPoint(points[i].Id, points[i].X, points[i].Y);
                        MyPoint p2 = new MyPoint(points[j].Id, points[j].X, points[j].Y);
                        // Додаємо ребро з вагою (відстанню)
                        edges.Add(new Edge(p1, p2, EdgeLength(p1, p2)));
                    }
                }
            }
        }

        // Малювання всіх вершин
        private void DrawPoints()
        {
            for (int i = 0; i < points.Count; i++)
            {
                DrawPoint(points[i], $"{i}"); // Малюємо кожну вершину з номером
            }
        }

        // Малювання всіх ребер разом з підписами (вагами)
        private void DrawEdges()
        {
            foreach (var edge in edges)
            {
                MyPoint p1 = edge.Point1;
                MyPoint p2 = edge.Point2;

                // Обчислюємо текст підпису (вага ребра)
                var label = EdgeLength(p1, p2).ToString();
                var font = new Font(FontName, EdgeLabelFontSize);
                // Визначаємо розмір тексту для центрування
                var size = graphics.MeasureString(label, font, pictureBox.Size);

                // Малюємо лінію ребра темно-синім кольором
                graphics.DrawLine(new Pen(Color.Navy, EdgeThickness),
                    new Point(p1.X, p1.Y), new Point(p2.X, p2.Y));

                // Малюємо текст ваги посередині ребра
                graphics.DrawString(label, font, new SolidBrush(Color.Navy),
                    (p1.X + p2.X) / 2 - size.Width / 2,
                    (p1.Y + p2.Y) / 2 - size.Height / 2);
            }
        }

        // Очищення графа: видалення всіх вершин і ребер, очищення таблиці
        private void ClearGraph()
        {
            graphics.Clear(Color.Transparent); // Очищаємо графічний контекст
            this.pictureBox.BackgroundImage = bitmap; // Встановлюємо очищений bitmap як фон
            this.pictureBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch;
            points.Clear(); // Очищаємо список вершин
            Setka();        // Перемальовуємо сітку
            dataGridViewPoints.Rows.Clear(); // Очищаємо таблицю вершин
        }

        // Очищення матриці суміжності
        private void ClearMatrix()
        {
            dataGridViewMatrix.Rows.Clear();
            dataGridViewMatrix.Columns.Clear();
        }

        // Обробник кліку по PictureBox: додавання нової вершини
        private void pictureBox_Click(object sender, EventArgs e)
        {
            // Обчислюємо координати кліку з урахуванням положення PictureBox на формі
            int x = MousePosition.X - Location.X - pictureBox.Location.X - 8;
            int y = MousePosition.Y - Location.Y - pictureBox.Location.Y - 32;

            // Додаємо нову вершину з автоматичним номером
            points.Add(new MyPoint(points.Count, x, y));

            // Додаємо запис у таблицю вершин
            dataGridViewPoints.Rows.Add(points[points.Count - 1].Id, $"({x}; {y})");

            // Малюємо щойно додану вершину
            DrawPoint(points[points.Count - 1], $"{points.Count - 1}");
        }

        // Обробник пункту меню "Новий граф" (побудова графа)
        private void newGraphToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // Якщо матриця ще не створена або її розмір не відповідає кількості вершин
            if (dataGridViewMatrix.Rows.Count < dataGridViewPoints.Rows.Count)
                GenerateMatrix(); // Генеруємо матрицю суміжності

            GenerateEdges();  // Формуємо ребра на основі матриці
            DrawPoints();     // Малюємо вершини
            DrawEdges();      // Малюємо ребра

            this.pictureBox.Invalidate(); // Примусово перемальовуємо PictureBox
        }

        // Обробник пункту меню "Очистити" (повне очищення)
        private void clearToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ClearGraph();   // Очищаємо граф
            ClearMatrix();  // Очищаємо матрицю
        }

        // Обробник пункту меню "Згенерувати матрицю"
        private void generateMatrixToolStripMenuItem_Click(object sender, EventArgs e)
        {
            GenerateMatrix(); // Створюємо порожню матрицю суміжності
        }

        // Малювання координатної сітки на графічному контексті
        protected void Setka()
        {
            Pen pen = new Pen(Color.Chartreuse, EdgeThickness);
            graphics.Clear(Color.Transparent); // Очищаємо попереднє зображення

            // Малюємо межі області (рамку)
            Point P0 = new Point(1, 500); Point P1 = new Point(500, 500);
            Point P2 = new Point(1, 1); Point P3 = new Point(500, 1);
            graphics.DrawLine(pen, P0, P1); graphics.DrawLine(pen, P2, P3);
            graphics.DrawLine(pen, P0, P2); graphics.DrawLine(pen, P3, P1);

            // Налаштовуємо тонку сіру сітку з кроком 10 пікселів
            pen.Width = 1;
            pen.Color = Color.FromArgb(50, 0, 0, 0);
            for (int i = 0; i < 501; i = i + 10)
            {
                graphics.DrawLine(pen, i, 0, i, 500);   // Вертикальні лінії
                graphics.DrawLine(pen, 0, i, 500, i);   // Горизонтальні лінії
            }

            // Оновлюємо зображення
            pictureBox.Image = bitmap;
        }

        // Перевизначений метод OnPaint: викликається при необхідності перемалювання форми
        protected override void OnPaint(PaintEventArgs e)
        {
            Setka(); // Малюємо сітку

            // Встановлюємо bitmap як фон PictureBox
            this.pictureBox.BackgroundImage = bitmap;
            this.pictureBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch;

            DrawPoints(); // Малюємо вершини
            DrawEdges();  // Малюємо ребра

            base.OnPaint(e);
        }

        // Обробник пункту меню "Вихід"
        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Application.Exit(); // Завершуємо роботу програми
        }

        // Обробник пункту меню "Карта" (завантаження фонового зображення)
        private void mapsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            string path_image = OpenImageFile(); // Відкриваємо діалог вибору файлу
            if (string.IsNullOrEmpty(path_image)) { return; } // Якщо файл не обрано, виходимо

            Bitmap bitmap = new Bitmap(path_image); // Завантажуємо зображення
            this.pictureBox.BackgroundImage = bitmap; // Встановлюємо як фон
            this.pictureBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch;
            Setka(); // Малюємо сітку поверх фону
        }
    }
}

Крок 2. Повний код класу Program

using System;
using System.Windows.Forms;

namespace GraphVisualization
{
    static class Program
    {
        /// 
        /// The main entry point for the application.
        /// 
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

Висновки

Ми розглянули основи теорії графів, способи їх представлення та створили простий застосунок для візуалізації неорієнтованого графа на основі матриці суміжності. Цей проєкт є основою для подальшого розвитку: додавання орієнтованих ребер, ваг, алгоритмів пошуку шляхів тощо.