Derangement Finder Desktop App – A Smart Tool to Explore Permutations Without Fixed Points

 

Derangement Finder Desktop App – A Smart Tool to Explore Permutations Without Fixed Points

Understanding permutations is an essential part of combinatorics, probability, and computer science—but one special type of permutation often confuses students and professionals alike: derangements. A derangement is a permutation where no element appears in its original position. This fascinating concept forms the basis of problems such as the classic “Hat Check Problem,” random assignments, encryption schemes, and error-correcting methods.

To make learning and experimenting with derangements easier, the Derangement Finder Desktop App provides an interactive and user-friendly platform. Whether you're a student preparing for competitive exams, a teacher explaining combinatorics, or a programmer working with permutations, this tool simplifies everything with just a few clicks.


What Is a Derangement? (Simple Explanation)

A derangement of a set is a rearrangement where nothing stays where it originally was.

For example, for items:
["A", "B", "C"]

Valid derangements are:

  • ["B", "C", "A"]

  • ["C", "A", "B"]

Invalid examples (because at least one item stayed in its original index):

  • ["A", "C", "B"] (A stayed in position 0)

  • ["B", "A", "C"] (C stayed in position 2)

The number of derangements grows in an interesting pattern and is closely related to the mathematical constant e.


Introducing the Derangement Finder Desktop App

The Derangement Finder is a simple yet powerful Python desktop application that helps users:

✔ Calculate exact derangement counts (!n)
✔ Generate all derangements (for small sets)
✔ Generate a random derangement instantly
✔ Enter custom items or generate random sets
✔ Export derangements to text or CSV files
✔ View and copy results easily in a clean GUI

The app is written in Python using the Tkinter GUI library, which means it runs on almost any system without requiring heavy installations.


Key Features of the App

1. Compute Derangement Count Instantly

Just enter the number of items or provide a list of custom elements.
The app uses the inclusion–exclusion formula to compute the derangement count accurately:

!n=n!k=0n(1)kk!!n = n! \sum_{k=0}^{n} \frac{(-1)^k}{k!}

This gives you fast, reliable results—even for larger values.


2. Generate All Derangements

For smaller sets (usually 8–10 items), the app can produce every derangement using a backtracking algorithm.
This is perfect for:

  • Students learning permutation concepts

  • Teachers demonstrating combinatorics

  • Programmers testing algorithms

All results appear neatly in the list section of the application.


3. Random Derangement Generator

If you only need one derangement—not the full list—this feature gives you a valid random derangement instantly.

The app uses optimized logic so you always receive a valid permutation with no fixed points.


4. Export as TXT or CSV

Want to use the results in assignments, tests, or projects?

You can export derangements to:

  • .txt files

  • .csv files

Perfect for documentation, teaching materials, or further analysis in Excel or Python.


Why This App Is Useful

✔ Perfect for Students

Makes derangements easy to understand with hands-on examples.

✔ Great for Teachers

Visual demonstrations help explain abstract concepts clearly.

✔ Helpful for Programmers

Useful for algorithm testing, simulations, and randomized assignments.

✔ Excellent for Competitive Exams

Derangement questions commonly appear in:

  • GATE

  • IIT-JEE

  • SSC

  • Banking exams

  • University mathematics papers

This tool helps you practice quickly and accurately.


How to Run the App

  1. Save the Python file as:
    derangement_finder.py

  2. Ensure Python 3.7+ is installed.

  3. Run the program:

    python derangement_finder.py

Tkinter comes pre-installed with most Python distributions, so no extra installation is needed.


Conclusion

The Derangement Finder Desktop App is designed for anyone who wants to understand, explore, or experiment with derangements. Its clean interface, powerful features, and export options make it a valuable tool for students, teachers, and developers alike.

"""

Derangement Finder — Desktop App (Tkinter)

Save as: derangement_finder.py

Run: python derangement_finder.py


Features:

- Enter a list of items (comma-separated) or use a sample/random list

- Compute exact number of derangements (!n) using inclusion-exclusion

- Generate all derangements (backtracking) with a safety warning for large n

- Generate a random derangement quickly

- Display derangements in a scrollable list, copy to clipboard, export to TXT/CSV

- Filter derangements by pattern or index

- Lightweight Tkinter GUI; standard-library only


Notes:

- Derangements are permutations where no element remains in its original position.

- For n > 10 the number of derangements grows large; listing all may be slow and memory-heavy.


Author: Generated by ChatGPT

"""


import tkinter as tk

from tkinter import ttk, messagebox, filedialog

import math

import random

import csv



def derangement_count(n: int) -> int:

    """Exact derangement count using inclusion-exclusion:

    !n = n! * sum_{k=0..n} (-1)^k / k! = round(n!/e)

    We'll compute the integer sum to avoid floating rounding issues.

    """

    if n < 0:

        return 0

    fact = math.factorial(n)

    s = 0

    for k in range(0, n + 1):

        s += ((-1) ** k) // 1 * (1 / math.factorial(k)) if False else 0

    # The above line is intentionally not used (avoid floats). Use integer inclusion-exclusion:

    tot = 0

    for k in range(0, n + 1):

        term = ((-1) ** k) * math.factorial(n) // math.factorial(k)

        tot += term

    # tot is n! * sum_{k=0..n} (-1)^k / k! but integer division used - safe because factorial(k) divides n!.

    return tot



def derangement_count_ie(n: int) -> int:

    """Cleaner exact inclusion-exclusion implementation returning integer !n."""

    if n < 0:

        return 0

    tot = 0

    fact_n = math.factorial(n)

    for k in range(0, n + 1):

        # n! / k! * (-1)^k

        tot += ((-1) ** k) * (fact_n // math.factorial(k))

    return tot



def random_derangement(items):

    """Generate a random derangement using rejection sampling for moderate n.

    For larger n, use a constructive algorithm: start with indices and perform

    a random derangement generation (Sattolo's algorithm doesn't guarantee derangement if there are duplicates in items),

    so we use a simple algorithm that shuffles until valid (retries limited), then fallback to constructive method.

    """

    n = len(items)

    if n == 0:

        return []

    max_tries = 2000

    indices = list(range(n))

    for _ in range(max_tries):

        random.shuffle(indices)

        if all(indices[i] != i for i in range(n)):

            return [items[i] for i in indices]

    # fallback constructive: greedy with random choices with backtracking

    return _construct_derangement(items)



def _construct_derangement(items):

    n = len(items)

    original = list(items)


    result = [None] * n

    used = [False] * n


    def backtrack(pos):

        if pos == n:

            return True

        choices = list(range(n))

        random.shuffle(choices)

        for j in choices:

            if used[j]:

                continue

            if j == pos:

                continue

            # choose j at position pos

            used[j] = True

            result[pos] = original[j]

            if backtrack(pos + 1):

                return True

            used[j] = False

            result[pos] = None

        return False


    ok = backtrack(0)

    if not ok:

        # If impossible (shouldn't be for n>1), return empty

        return []

    return result



def all_derangements(items, limit=None):

    """Generate all derangements with backtracking. If limit is provided, stop after reaching it.

    This guarantees no element remains in original position (by index comparison).

    """

    n = len(items)

    original = list(items)

    result = []

    used = [False] * n

    current = [None] * n


    def backtrack(pos):

        if limit is not None and len(result) >= limit:

            return

        if pos == n:

            result.append(list(current))

            return

        for j in range(n):

            if used[j]:

                continue

            if j == pos:

                continue

            used[j] = True

            current[pos] = original[j]

            backtrack(pos + 1)

            used[j] = False

            current[pos] = None

            if limit is not None and len(result) >= limit:

                return


    backtrack(0)

    return result



class DerangementFinderApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("Derangement Finder")

        self.geometry("880x520")

        self.resizable(True, True)


        self.items = []

        self.derangements = []


        self._build_ui()


    def _build_ui(self):

        frm = ttk.Frame(self, padding=10)

        frm.pack(fill=tk.BOTH, expand=True)


        top = ttk.Frame(frm)

        top.pack(fill=tk.X)


        ttk.Label(top, text="Enter items (comma-separated):").pack(side=tk.LEFT)

        self.entry_var = tk.StringVar(value="A, B, C")

        self.entry = ttk.Entry(top, textvariable=self.entry_var, width=50)

        self.entry.pack(side=tk.LEFT, padx=8)


        ttk.Button(top, text="Random Set", command=self.random_set).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Compute Count", command=self.compute_count).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Generate All", command=self.generate_all).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Random Derangement", command=self.show_random_derangement).pack(side=tk.LEFT, padx=4)


        # Middle: listbox + details

        mid = ttk.Panedwindow(frm, orient=tk.HORIZONTAL)

        mid.pack(fill=tk.BOTH, expand=True, pady=10)


        left = ttk.Frame(mid)

        mid.add(left, weight=2)


        self.listbox = tk.Listbox(left, font=("Consolas", 11))

        self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.listbox.bind('<<ListboxSelect>>', lambda e: self.on_select())


        scroll = ttk.Scrollbar(left, orient=tk.VERTICAL, command=self.listbox.yview)

        scroll.pack(side=tk.RIGHT, fill=tk.Y)

        self.listbox.config(yscrollcommand=scroll.set)


        right = ttk.Frame(mid)

        mid.add(right, weight=1)


        ttk.Label(right, text="Selected Derangement:").pack(anchor=tk.W)

        self.text_selected = tk.Text(right, height=8, wrap=tk.WORD, state=tk.DISABLED)

        self.text_selected.pack(fill=tk.X)


        btn_frame = ttk.Frame(right)

        btn_frame.pack(fill=tk.X, pady=8)

        ttk.Button(btn_frame, text="Copy", command=self.copy_selected).pack(side=tk.LEFT, padx=4)

        ttk.Button(btn_frame, text="Export TXT", command=self.export_txt).pack(side=tk.LEFT, padx=4)

        ttk.Button(btn_frame, text="Export CSV", command=self.export_csv).pack(side=tk.LEFT, padx=4)


        ttk.Separator(right, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=6)

        self.info_lbl = ttk.Label(right, text="No set loaded.")

        self.info_lbl.pack(anchor=tk.W)


    def parse_items(self):

        raw = self.entry_var.get()

        items = [it.strip() for it in raw.split(',') if it.strip()]

        return items


    def random_set(self):

        n = random.randint(2, 7)

        sample = [chr(65 + i) for i in range(n)]

        self.entry_var.set(', '.join(sample))

        self.items = sample

        self.info_lbl.config(text=f'Random set loaded: {len(self.items)} items')

        self.derangements = []

        self.listbox.delete(0, tk.END)


    def compute_count(self):

        items = self.parse_items()

        if not items:

            messagebox.showwarning('No items', 'Please enter at least one item.')

            return

        n = len(items)

        cnt = derangement_count_ie(n)

        self.info_lbl.config(text=f'Items: {n} | Derangement count (!n): {cnt}')

        messagebox.showinfo('Derangement Count', f'For n={n}, derangement count (!n) = {cnt}')


    def generate_all(self):

        items = self.parse_items()

        if not items:

            messagebox.showwarning('No items', 'Please enter at least one item.')

            return

        n = len(items)

        cnt = derangement_count_ie(n)

        if cnt > 20000:

            if not messagebox.askyesno('Large output', f'There are {cnt} derangements for n={n}. Listing all may be slow or use a lot of memory. Continue?'):

                return

        # generate all

        self.items = items

        self.derangements = all_derangements(items)

        self.listbox.delete(0, tk.END)

        for idx, d in enumerate(self.derangements, 1):

            self.listbox.insert(tk.END, f'{idx:5d}: {{ ' + ', '.join(d) + ' }}')

        self.info_lbl.config(text=f'Loaded {n} items; derangements listed: {len(self.derangements)}')


    def show_random_derangement(self):

        items = self.parse_items()

        if not items:

            messagebox.showwarning('No items', 'Please enter at least one item.')

            return

        if len(items) == 1:

            messagebox.showinfo('No derangement', 'A single-item set has 0 derangements.')

            return

        der = random_derangement(items)

        if not der:

            messagebox.showerror('Error', 'Could not construct a derangement.')

            return

        # show in dialog and add to top of listbox

        messagebox.showinfo('Random Derangement', '{ ' + ', '.join(der) + ' }')

        # add to listbox for convenience

        self.listbox.insert(0, 'rand: { ' + ', '.join(der) + ' }')

        self.info_lbl.config(text=f'Random derangement generated for n={len(items)}')


    def on_select(self):

        sel = self.listbox.curselection()

        if not sel:

            return

        idx = sel[0]

        text = self.listbox.get(idx)

        self.text_selected.config(state=tk.NORMAL)

        self.text_selected.delete('1.0', tk.END)

        self.text_selected.insert(tk.END, text + '\n')

        self.text_selected.config(state=tk.DISABLED)


    def copy_selected(self):

        sel = self.listbox.curselection()

        if not sel:

            messagebox.showinfo('No selection', 'Please select a derangement first.')

            return

        idx = sel[0]

        text = self.listbox.get(idx)

        self.clipboard_clear()

        self.clipboard_append(text)

        messagebox.showinfo('Copied', 'Selected derangement copied to clipboard.')


    def export_txt(self):

        if not self.derangements and self.listbox.size() == 0:

            messagebox.showinfo('No data', 'No derangements to export. Generate or create a random derangement first.')

            return

        path = filedialog.asksaveasfilename(defaultextension='.txt', filetypes=[('Text files', '*.txt')])

        if not path:

            return

        with open(path, 'w', encoding='utf-8') as f:

            if self.derangements:

                for d in self.derangements:

                    f.write('{ ' + ', '.join(d) + ' }\n')

            else:

                # export items currently in listbox

                for i in range(self.listbox.size()):

                    f.write(self.listbox.get(i) + '\n')

        messagebox.showinfo('Exported', f'Derangements exported to {path}')


    def export_csv(self):

        if not self.derangements and self.listbox.size() == 0:

            messagebox.showinfo('No data', 'No derangements to export. Generate or create a random derangement first.')

            return

        path = filedialog.asksaveasfilename(defaultextension='.csv', filetypes=[('CSV files', '*.csv')])

        if not path:

            return

        with open(path, 'w', newline='', encoding='utf-8') as f:

            writer = csv.writer(f)

            if self.derangements:

                for d in self.derangements:

                    writer.writerow(d)

            else:

                for i in range(self.listbox.size()):

                    row = [part.strip() for part in self.listbox.get(i).split(':', 1)[-1].strip(' {}').split(',')]

                    writer.writerow(row)

        messagebox.showinfo('Exported', f'Derangements exported to {path}')



if __name__ == '__main__':

    app = DerangementFinderApp()

    app.mainloop()

https://github.com/gagandeep44489/DiscreteStrucutreAndAlgoApp/blob/main/Derangement%20Finder.py

Comments

Popular posts from this blog

NAND / NOR Logic Simulator: A Python Desktop App for Understanding Universal Logic Gates

Subset Sum Problem Visualizer Using Python (Dynamic Programming GUI Tool)

String Matching Algorithm Trainer (KMP & Rabin-Karp) – Python Desktop App