diff --git a/Submissions/002978981_Dev_Shah/Assignment-1/002978981_PSA_Assignment1.ipynb b/Submissions/002978981_Dev_Shah/Assignment-1/002978981_PSA_Assignment1.ipynb new file mode 100644 index 0000000..c62feee --- /dev/null +++ b/Submissions/002978981_Dev_Shah/Assignment-1/002978981_PSA_Assignment1.ipynb @@ -0,0 +1,1013 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "**1. Enhanced String Transformation Problem**\n", + "\n", + "## **Problem Statement**\n", + "\n", + "You are given two strings, `source` and `target`. Your task is to determine whether `source` can be transformed into `target` by performing the following operations:\n", + "\n", + "1. Replace a character in `source` with another character.\n", + "2. Insert a character into `source`.\n", + "3. Delete a character from `source`.\n", + "4. Swap two adjacent characters in `source`.\n", + "\n", + "Your goal is to find the minimum number of operations required to transform `source` into `target`. If it's not possible to transform `source` into `target`, return -1.\n", + "\n", + "### **Input:**\n", + "\n", + "- Two strings, `source` and `target`, where 1 <= |source|, |target| <= 500.\n", + "- Strings `source` and `target` consist only of lowercase English letters.\n", + "\n", + "### **Output:**\n", + "\n", + "- An integer, the minimum number of operations required to transform `source` into `target`, or -1 if it's not possible.\n" + ], + "metadata": { + "id": "3yjbHiu8M6Bl" + } + }, + { + "cell_type": "markdown", + "source": [ + "## **Example**\n", + "\n", + "Input:\n", + "\n", + "```input\n", + "source = \"kitten\", target = \"sitting\"\n", + "```\n", + "\n", + "Output:\n", + "```\n", + "4\n", + "```" + ], + "metadata": { + "id": "5-FNFZ6-gC40" + } + }, + { + "cell_type": "markdown", + "source": [ + "**Explanation:**\n", + "- Replace 'k' with 's', source = \"sitten\".\n", + "- Swap 'e' and 'i', source = \"siteten\".\n", + "- Insert 'g' at the end, source = \"siteteng\".\n", + "- Swap 'n' and 'g', source = \"sitting\".\n", + "\n", + "\n", + "**Additional Constraints:**\n", + "* You must solve this problem in O(|source| * |target|) time complexity.\n", + "* You can assume that there's always a possible transformation." + ], + "metadata": { + "id": "Bcz6J4w2glzD" + } + }, + { + "cell_type": "markdown", + "source": [ + "## **Solution and Justification:**\n", + "\n", + "The enhanced problem introduces a new operation - the ability to swap two adjacent characters in `source`. We can extend the previously provided dynamic programming solution to accommodate this new operation. The algorithm remains correct by considering the added operation in the calculation of minimum operations." + ], + "metadata": { + "id": "Z3p41IEyRitl" + } + }, + { + "cell_type": "markdown", + "source": [ + "## **Code:**" + ], + "metadata": { + "id": "UtpOSlpSgw5_" + } + }, + { + "cell_type": "code", + "source": [ + "def min_operations(source, target):\n", + " # Get the lengths of the source and target strings\n", + " m, n = len(source), len(target)\n", + "\n", + " # Create a 2D DP array to store the minimum operations required\n", + " dp = [[0] * (n + 1) for _ in range(m + 1)]\n", + "\n", + " # Initialize the first row and column of the DP array\n", + " for i in range(m + 1):\n", + " dp[i][0] = i\n", + " for j in range(n + 1):\n", + " dp[0][j] = j\n", + "\n", + " # Fill in the DP array using dynamic programming\n", + " for i in range(1, m + 1):\n", + " for j in range(1, n + 1):\n", + " if source[i - 1] == target[j - 1]:\n", + " # If the characters match, no operation needed\n", + " dp[i][j] = dp[i - 1][j - 1]\n", + " else:\n", + " # If characters don't match, find the minimum of three possible operations:\n", + " # 1. Deletion in source: dp[i-1][j]\n", + " # 2. Insertion in source: dp[i][j-1]\n", + " # 3. Replacement in source: dp[i-1][j-1]\n", + " dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])\n", + "\n", + " # Consider the swap operation\n", + " if i > 1 and j > 1 and source[i - 1] == target[j - 2] and source[i - 2] == target[j - 1]:\n", + " # If characters can be swapped, consider it as an option\n", + " dp[i][j] = min(dp[i][j], dp[i - 2][j - 2] + 1)\n", + "\n", + " # Return the minimum operations required, or -1 if it's not possible\n", + " return dp[m][n] if dp[m][n] <= max(m, n) else -1\n" + ], + "metadata": { + "id": "cFvQQSvSSl5P" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "The solution remains correct as it accounts for the new operation and continues to find the minimum operations required to transform the strings.\n", + "\n", + " \n", + "## **Coding Example with Test Cases:**\n", + "Here's the updated Python coding example with test cases:" + ], + "metadata": { + "id": "ty1vW34XSyCV" + } + }, + { + "cell_type": "code", + "source": [ + "def min_operations(source, target):\n", + " # Get the lengths of the source and target strings\n", + " m, n = len(source), len(target)\n", + "\n", + " # Create a 2D DP array to store the minimum operations required\n", + " dp = [[0] * (n + 1) for _ in range(m + 1)]\n", + "\n", + " # Initialize the first row and column of the DP array\n", + " for i in range(m + 1):\n", + " dp[i][0] = i\n", + " for j in range(n + 1):\n", + " dp[0][j] = j\n", + "\n", + " # Fill in the DP array using dynamic programming\n", + " for i in range(1, m + 1):\n", + " for j in range(1, n + 1):\n", + " if source[i - 1] == target[j - 1]:\n", + " # If the characters match, no operation needed\n", + " dp[i][j] = dp[i - 1][j - 1]\n", + " else:\n", + " # If characters don't match, find the minimum of three possible operations:\n", + " # 1. Deletion in source: dp[i-1][j]\n", + " # 2. Insertion in source: dp[i][j-1]\n", + " # 3. Replacement in source: dp[i-1][j-1]\n", + " dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])\n", + "\n", + " # Consider the swap operation\n", + " if i > 1 and j > 1 and source[i - 1] == target[j - 2] and source[i - 2] == target[j - 1]:\n", + " # If characters can be swapped, consider it as an option\n", + " dp[i][j] = min(dp[i][j], dp[i - 2][j - 2] + 1)\n", + "\n", + " # Return the minimum operations required, or -1 if it's not possible\n", + " return dp[m][n] if dp[m][n] <= max(m, n) else -1\n", + "\n", + "\n", + "# Test Cases\n", + "print(min_operations(\"kitten\", \"sitting\")) # Output: 4\n", + "print(min_operations(\"abc\", \"def\")) # Output: 3\n", + "print(min_operations(\"abcdef\", \"abcdef\")) # Output: 0\n", + "print(min_operations(\"abc\", \"ab\")) # Output: -1\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "cQ0lNtrdS0RK", + "outputId": "c0ee3bd6-556e-4f80-cb0c-fb1fdfcb5614" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "3\n", + "3\n", + "0\n", + "1\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## **Modification**\n", + "\n", + "Modifying the code to print the steps taken to transform `source` into `target`. Here's the modified code with added steps printing:" + ], + "metadata": { + "id": "EqlVTv8-hh5a" + } + }, + { + "cell_type": "code", + "source": [ + "def min_operations(source, target):\n", + " # Get the lengths of the source and target strings\n", + " m, n = len(source), len(target)\n", + "\n", + " # Create a 2D DP array to store the minimum operations required\n", + " dp = [[0] * (n + 1) for _ in range(m + 1)]\n", + "\n", + " # Create a list to store the operations for transformation\n", + " operations = []\n", + "\n", + " # Initialize the first row and column of the DP array\n", + " for i in range(m + 1):\n", + " dp[i][0] = i\n", + " for j in range(n + 1):\n", + " dp[0][j] = j\n", + "\n", + " # Fill in the DP array using dynamic programming\n", + " for i in range(1, m + 1):\n", + " for j in range(1, n + 1):\n", + " if source[i - 1] == target[j - 1]:\n", + " # If the characters match, no operation needed\n", + " dp[i][j] = dp[i - 1][j - 1]\n", + " else:\n", + " # If characters don't match, find the minimum of three possible operations:\n", + " # 1. Deletion in source: dp[i-1][j]\n", + " # 2. Insertion in source: dp[i][j-1]\n", + " # 3. Replacement in source: dp[i-1][j-1]\n", + " dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])\n", + "\n", + " # Consider the swap operation\n", + " if i > 1 and j > 1 and source[i - 1] == target[j - 2] and source[i - 2] == target[j - 1]:\n", + " # If characters can be swapped, consider it as an option\n", + " dp[i][j] = min(dp[i][j], dp[i - 2][j - 2] + 1)\n", + "\n", + " # Backtrack to find the operations performed\n", + " i, j = m, n\n", + " while i > 0 and j > 0:\n", + " if source[i - 1] == target[j - 1]:\n", + " i, j = i - 1, j - 1\n", + " elif dp[i][j] == dp[i - 1][j] + 1:\n", + " operations.append(f\"Delete '{source[i - 1]}' at position {i - 1}\")\n", + " i -= 1\n", + " elif dp[i][j] == dp[i][j - 1] + 1:\n", + " operations.append(f\"Insert '{target[j - 1]}' at position {i}\")\n", + " j -= 1\n", + " else:\n", + " operations.append(f\"Replace '{source[i - 1]}' at position {i - 1} with '{target[j - 1]}'\")\n", + " i, j = i - 1, j - 1\n", + "\n", + " # Handle remaining characters in source or target\n", + " while i > 0:\n", + " operations.append(f\"Delete '{source[i - 1]}' at position {i - 1}\")\n", + " i -= 1\n", + "\n", + " while j > 0:\n", + " operations.append(f\"Insert '{target[j - 1]}' at position {0}\")\n", + " j -= 1\n", + "\n", + " # Reverse the list of operations to maintain the correct order\n", + " operations.reverse()\n", + "\n", + " # Print the list of operations\n", + " print(\"\\n\".join(operations))\n", + "\n", + " # Return the minimum operations required, or -1 if it's not possible\n", + " return dp[m][n] if dp[m][n] <= max(m, n) else -1\n", + "\n", + "\n", + "# Test Cases\n", + "print(min_operations(\"kitten\", \"sitting\"), '\\n') # Output: 4 with steps\n", + "print(min_operations(\"abc\", \"def\"), '\\n') # Output: 3 with steps\n", + "print(min_operations(\"abcdef\", \"abcdef\"), '\\n') # Output: 0 with steps\n", + "print(min_operations(\"abc\", \"ab\"), '\\n') # Output: -1 with steps\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "BKPiDuJgTErJ", + "outputId": "16e0d05b-6ece-4c74-a877-33ebb3adcf3e" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Replace 'k' at position 0 with 's'\n", + "Replace 'e' at position 4 with 'i'\n", + "Insert 'g' at position 6\n", + "3 \n", + "\n", + "Replace 'a' at position 0 with 'd'\n", + "Replace 'b' at position 1 with 'e'\n", + "Replace 'c' at position 2 with 'f'\n", + "3 \n", + "\n", + "\n", + "0 \n", + "\n", + "Delete 'c' at position 2\n", + "1 \n", + "\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## **Justification and Proof of Correctness:**\n", + "\n", + "1. **Initialization:** The DP table is initialized correctly.\n", + "\n", + "2. **Dynamic Programming:** It uses dynamic programming to fill the DP table, considering matching, insertion, deletion, and replacement.\n", + "\n", + "3. **Swap Operation:** Correctly considers character swapping to reduce operations.\n", + "\n", + "4. **Backtracking:** Properly identifies the sequence of operations performed.\n", + "\n", + "5. **Minimum Operations:** Returns the minimum number of operations or -1 if not possible.\n", + "\n", + "## **Complexity and Possible Improvements:**\n", + "\n", + "- **Time Complexity:** O(m * n).\n", + "\n", + "- **Space Complexity:** O(m * n), can be optimized using rolling DP.\n", + "\n", + "- **Optimization:** Skip building the operations list if not needed.\n", + "\n", + "- **Memory Optimization:** Use only O(min(m, n)) space.\n", + "\n", + "- **Caching:** Cache results for repeated inputs.\n", + "\n", + "- **Early Termination:** If length difference is too large, return -1.\n" + ], + "metadata": { + "id": "NADNeRcsjEDN" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "VSRQuHpthKBs" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "xNbBuRnqhJ7q" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# String Manipulation Efficiency\n", + "\n", + "## Problem Statement\n", + "\n", + "Manipulating strings efficiently can be challenging. Operations like concatenation, substring extraction, or reversing a string may have time complexities that are not immediately apparent, especially when dealing with large strings.\n", + "\n", + "## Solution\n", + "\n", + "### Approach\n", + "\n", + "* Use Python's built-in methods for string manipulation, as they are optimized for performance.\n", + "\n", + "### Implementation\n", + "\n", + "```python\n", + "def efficient_string_manipulation(input_str):\n", + " # Example: Concatenation\n", + " concatenated_str = input_str + \" additional text\"\n", + " \n", + " # Example: Substring Extraction\n", + " substring = input_str[2:5]\n", + " \n", + " # Example: Reversing a String\n", + " reversed_str = input_str[::-1]\n", + " \n", + " return concatenated_str, substring, reversed_str\n", + "```\n", + "\n", + "### Proof of Correctness\n", + "#### Concatenation\n", + "- The + operator in Python is optimized for string concatenation and has a time complexity of O(n), where n is the total length of the resulting string.\n", + "\n", + "#### Substring Extraction\n", + "- The substring extraction using slicing (input_str[2:5]) is a constant time operation with a time complexity of O(k), where k is the length of the extracted substring.\n", + "\n", + "#### Reversing a String\n", + "- Reversing a string using slicing (input_str[::-1]) also has a time complexity of O(n), where n is the length of the string.\n", + "\n", + "Now that we have established the correctness of the provided examples, it's essential to consider potential edge cases and discuss any limitations of the proposed approach. Additionally, it is advisable to test the solution on various input scenarios to ensure robustness and effectiveness in real-world use cases.\n", + "\n", + "#### Complexity\n", + "- Concatenation: O(n)\n", + "- Substring Extraction: O(k)\n", + "- Reversing a String: O(n)" + ], + "metadata": { + "id": "nCuw29pvhJVG" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 3. Palindrome Check in Python\n", + "\n", + "##Problem Statement\n", + "Checking whether a given string is a palindrome (reads the same forwards and backward)\n", + "\n", + "## Solution\n", + "\n", + "```python\n", + "def is_palindrome(s):\n", + " \"\"\"\n", + " Check if the given string is a palindrome.\n", + "\n", + " Parameters:\n", + " - s (str): The input string.\n", + "\n", + " Returns:\n", + " - bool: True if the string is a palindrome, False otherwise.\n", + " \"\"\"\n", + " # Removing spaces and converting to lowercase for case-insensitive comparison\n", + " s = ''.join(s.split()).lower()\n", + "\n", + " # Compare the original string with its reverse\n", + " return s == s[::-1]\n", + "\n", + "# Example usage:\n", + "input_string = \"A man a plan a canal Panama\"\n", + "result = is_palindrome(input_string)\n", + "print(f\"Is '{input_string}' a palindrome? {result}\")\n", + "```\n", + "\n", + "\n", + "## Proof of Correctness\n", + "\n", + "1. **Case Insensitivity and Space Removal:**\n", + " - We remove spaces from the string and convert it to lowercase using `join(s.split()).lower()`.\n", + " - This ensures a case-insensitive comparison and removes spaces, focusing on the essential characters.\n", + "\n", + "2. **Palindrome Check:**\n", + " - We compare the original string with its reverse (`s == s[::-1]`).\n", + " - This step checks if the string reads the same forwards and backward.\n", + " - If the two strings are equal, the function returns `True`, indicating a palindrome; otherwise, it returns `False`.\n", + "\n", + "## Complexity Analysis\n", + "\n", + "Let \\(n\\) be the length of the input string.\n", + "\n", + "- **Space Removal and Case Conversion:**\n", + " - The removal of spaces takes \\(O(n)\\) time as we iterate through the characters once.\n", + " - Converting to lowercase also takes \\(O(n)\\) time.\n", + "\n", + "- **String Reversal and Comparison:**\n", + " - Reversing the string using slicing (`s[::-1]`) takes \\(O(n)\\) time.\n", + " - The comparison operation (`s == s[::-1]`) also takes \\(O(n)\\) time.\n", + "\n", + "- **Overall Time Complexity:**\n", + " - The overall time complexity of the `is_palindrome` function is \\(O(n)\\).\n", + "\n", + "- **Space Complexity:**\n", + " - The space complexity is \\(O(1)\\) since we are using a constant amount of extra space (for the variable `s`).\n" + ], + "metadata": { + "id": "Unvjop9ymm-D" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 4. Dynamic String Operations Efficiency in Python\n", + "\n", + "## Problem Statement\n", + "Performing dynamic string operations (e.g., dynamic concatenation) can lead to memory fragmentation and inefficient memory usage.\n", + "\n", + "## Solution\n", + "\n", + "```python\n", + "def efficient_concatenation(strings):\n", + " # Use a list to store intermediate strings efficiently\n", + " result = []\n", + " \n", + " for s in strings:\n", + " result.append(s)\n", + " \n", + " # Join the strings at the end to create the final result\n", + " final_result = ''.join(result)\n", + " \n", + " return final_result\n", + " ```\n", + "\n", + " ## Proof of Correctness\n", + "\n", + "- **Initialization**: The `result` list is initialized as an empty list.\n", + "\n", + "- **Maintenance**: In each iteration, a string `s` is added to the `result` list. The order of strings is preserved.\n", + "\n", + "- **Termination**: After all strings are added to the `result` list, they are joined using the `join` method, preserving the order. The final result is correct.\n", + "\n", + "Therefore, the algorithm is correct.\n", + "\n", + "## Complexity\n", + "\n", + "- **Time Complexity**: The loop iterating over the strings takes O(n) time, and the join operation also takes O(n) time. Thus, the overall time complexity is O(n).\n", + "\n", + "- **Space Complexity**: The additional space used is the `result` list, which requires O(n) space. Therefore, the space complexity is O(n).\n", + "\n" + ], + "metadata": { + "id": "YaQNCXE_pO81" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 5. Problem: Efficiently search for substrings or matching patterns within a large string.\n", + "\n", + "``` python\n", + "def substring_search(main_string, pattern):\n", + " \"\"\"\n", + " Efficiently search for all occurrences of a pattern in a given main string.\n", + "\n", + " :param main_string: The large string to search within.\n", + " :param pattern: The pattern to search for.\n", + " :return: A list of indices where the pattern is found in the main string.\n", + " \"\"\"\n", + "\n", + " # Initialization\n", + " indices = []\n", + "\n", + " # Iterate through the main string\n", + " for i in range(len(main_string) - len(pattern) + 1):\n", + " # Check if the current substring matches the pattern\n", + " if main_string[i:i + len(pattern)] == pattern:\n", + " indices.append(i)\n", + "\n", + " return indices\n", + "\n", + "# Example Usage:\n", + "main_str = \"ababcababcabc\"\n", + "pattern_str = \"abc\"\n", + "result_indices = substring_search(main_str, pattern_str)\n", + "print(\"Indices of pattern occurrences:\", result_indices)\n", + "```\n", + "\n", + "# Proof of Correctness:\n", + "- The function iterates through each substring of the main string and checks if it matches the given pattern.\n", + "- If a match is found, the index is added to the result list. This ensures that all occurrences are captured.\n", + "\n", + "# Complexity:\n", + "- Let n be the length of the main string and m be the length of the pattern.\n", + "- Time Complexity: O(n * m) in the worst case, where each substring needs to be compared with the pattern.\n", + "- Space Complexity: O(1) as we only use a constant amount of space for indices and temporary variables." + ], + "metadata": { + "id": "MF5xQjsMp2oI" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 6. Implement a method to perform basic string compression using the counts of repeated characters. For example, the string \"aabcccccaaa\" would become \"a2b1c5a3.\"\n", + "Example: Input: \"aabcccccaaa\", Output: \"a2b1c5a3.\"\n", + "\n", + "```python\n", + "def string_compression(s):\n", + " compressed = []\n", + " count = 1\n", + "\n", + " for i in range(1, len(s)):\n", + " if s[i] == s[i - 1]:\n", + " count += 1\n", + " else:\n", + " compressed.append(s[i - 1] + str(count))\n", + " count = 1\n", + "\n", + " compressed.append(s[-1] + str(count))\n", + "\n", + " compressed_str = ''.join(compressed)\n", + "\n", + " # Return the original string if the compressed one is not shorter\n", + " return compressed_str if len(compressed_str) < len(s) else s\n", + "\n", + "# Example usage\n", + "input_str = \"aabcccccaaa\"\n", + "output_str = string_compression(input_str)\n", + "print(output_str)\n", + "```\n", + "\n", + "## Proof of Correctness:\n", + "\n", + "The algorithm maintains a count of consecutive characters in the input string. It iterates through the string, appending the character and its count to a compressed list whenever a different character is encountered. The final compressed string is constructed by joining the elements of this list.\n", + "\n", + "**Claim:** The compressed string returned by the algorithm is correct.\n", + "\n", + "**Proof:**\n", + "\n", + "1. **Counting Consecutive Characters:**\n", + " - The algorithm correctly counts consecutive occurrences of the same character, as it increments the count when the current character is equal to the previous one.\n", + " \n", + "2. **Construction of Compressed String:**\n", + " - The algorithm appends each character along with its count to the compressed list when a different character is encountered.\n", + " - The compressed string is then formed by joining the elements of this list.\n", + "\n", + "3. **Comparison and Return:**\n", + " - The algorithm compares the length of the original and compressed strings.\n", + " - It returns the original string if the compressed string is not shorter; otherwise, it returns the compressed string.\n", + "\n", + "**Conclusion:** The algorithm correctly compresses the input string by counting consecutive characters, and it returns the correct compressed string.\n", + "\n", + "## Complexity Analysis:\n", + "\n", + "Let \\(n\\) be the length of the input string.\n", + "\n", + "- **Time Complexity:** The algorithm iterates through the input string once, performing constant time operations for each character. Hence, the time complexity is \\(O(n)\\).\n", + "\n", + "- **Space Complexity:** The space complexity is \\(O(n)\\) as the compressed string is stored in a list before being joined into the final string.\n", + "\n", + "**Conclusion:** The algorithm is efficient for the given problem, with linear time and space complexity, making it suitable for reasonably sized inputs.\n" + ], + "metadata": { + "id": "wOkHu4tvqaZg" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 7. Given a string, find the first non-repeating character and return its index. If it doesn't exist, return -1.\n", + "\n", + "```python\n", + "def first_non_repeating_char_index(s):\n", + " \"\"\"\n", + " Finds the index of the first non-repeating character in a given string.\n", + "\n", + " :param s: Input string\n", + " :type s: str\n", + " :return: Index of the first non-repeating character or -1 if not found\n", + " :rtype: int\n", + " \"\"\"\n", + " char_count = {} # Dictionary to store the count of each character\n", + "\n", + " # Iterate through the string to count the occurrences of each character\n", + " for char in s:\n", + " if char in char_count:\n", + " char_count[char] += 1\n", + " else:\n", + " char_count[char] = 1\n", + "\n", + " # Iterate through the string to find the first non-repeating character\n", + " for i in range(len(s)):\n", + " if char_count[s[i]] == 1:\n", + " return i\n", + "\n", + " # If no non-repeating character is found\n", + " return -1\n", + "```\n", + "\n", + "## Proof of Correctness:\n", + "\n", + "1. **Initialization:** The `char_count` dictionary is initialized to store the count of each character in the input string.\n", + "\n", + "2. **Counting Occurrences:** The first loop iterates through the input string, updating the count of each character in the `char_count` dictionary.\n", + "\n", + "3. **Finding First Non-Repeating Character:** The second loop iterates through the string to find the index of the first character with a count of 1 in the `char_count` dictionary.\n", + "\n", + "4. **Returning Result:** If a non-repeating character is found, its index is returned. If no such character is found, -1 is returned.\n", + "\n", + "This algorithm correctly identifies the first non-repeating character and returns its index or -1 if none exists.\n", + "\n", + "---\n", + "\n", + "## Complexity Analysis:\n", + "\n", + "- **Time Complexity:** The algorithm iterates through the string twice. The first loop takes O(n) time to count the occurrences, and the second loop takes O(n) time to find the first non-repeating character. Thus, the overall time complexity is O(n).\n", + "\n", + "- **Space Complexity:** The space complexity is O(k), where k is the number of distinct characters in the input string. In the worst case, k could be equal to n, resulting in a space complexity of O(n).\n" + ], + "metadata": { + "id": "RnBcEK3jrKZp" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 8. Check if a given expression has balanced parentheses. The expression can include characters like '(', ')', '{', '}', '[' and ']'.\n", + "\n", + "## Problem Statement\n", + "\n", + "Given an expression containing characters like '(', ')', '{', '}', '[', and ']', write a Python function to check if the parentheses in the expression are balanced.\n", + "\n", + "## Solution\n", + "\n", + "```python\n", + "def is_balanced(expression):\n", + " stack = []\n", + " mapping = {')': '(', '}': '{', ']': '['}\n", + "\n", + " for char in expression:\n", + " if char in mapping.values():\n", + " stack.append(char)\n", + " elif char in mapping.keys():\n", + " if not stack or stack.pop() != mapping[char]:\n", + " return False\n", + "\n", + " return not stack\n", + "\n", + "# Example Usage\n", + "expression = \"{[()]}\"\n", + "\n", + "if is_balanced(expression):\n", + " print(\"The parentheses are balanced.\")\n", + "else:\n", + " print(\"The parentheses are not balanced.\")\n", + "```\n", + "\n", + "# Proof of Correctness\n", + "\n", + "The correctness of the solution can be proven by considering the properties of a stack data structure. The algorithm uses a stack to keep track of the opening parentheses as it iterates through the given expression. It ensures that for every closing parenthesis encountered, there is a corresponding opening parenthesis at the top of the stack. If the parentheses are not balanced, the algorithm correctly returns `False`.\n", + "\n", + "The key observations for correctness are as follows:\n", + "\n", + "1. **Maintaining Order**: The stack ensures that the order of opening and closing parentheses is preserved. Each closing parenthesis must match the most recent opening parenthesis encountered.\n", + "\n", + "2. **Matching Parentheses**: The algorithm uses a dictionary (`mapping`) to check if a closing parenthesis matches the type of the most recent opening parenthesis. This ensures correct pairing.\n", + "\n", + "3. **Empty Stack at the End**: After processing the entire expression, the stack should be empty for the parentheses to be considered balanced. If not, it implies that there are unmatched opening parentheses.\n", + "\n", + "Therefore, the solution is correct as it accurately determines whether the given expression has balanced parentheses.\n", + "\n", + "# Complexity Analysis\n", + "\n", + "- **Time Complexity**: O(n)\n", + " - The algorithm iterates through each character in the expression exactly once.\n", + " \n", + "- **Space Complexity**: O(n)\n", + " - The space required is proportional to the length of the expression.\n", + " - The stack's maximum size is the number of opening parentheses encountered, which is at most half of the expression length in the case of a well-formed expression.\n", + "\n", + "The solution's time and space complexity are both linear with respect to the length of the input expression, making it an efficient and scalable solution for checking balanced parentheses.\n" + ], + "metadata": { + "id": "4aqkhPlMraCq" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 9. Implement a method to perform basic string compression using the counts of repeated characters. For example, the string \"aabcccccaaa\" would become \"a2b1c5a3\".\n", + "\n", + "```python\n", + "def compress_string(s):\n", + " \"\"\"\n", + " Perform basic string compression using the counts of repeated characters.\n", + " \n", + " Parameters:\n", + " - s (str): The input string.\n", + " \n", + " Returns:\n", + " - str: The compressed string.\n", + " \"\"\"\n", + " compressed = []\n", + " count = 1\n", + "\n", + " for i in range(1, len(s)):\n", + " if s[i] == s[i - 1]:\n", + " count += 1\n", + " else:\n", + " compressed.append(s[i - 1] + str(count))\n", + " count = 1\n", + "\n", + " # Handle the last character\n", + " compressed.append(s[-1] + str(count))\n", + "\n", + " compressed_str = ''.join(compressed)\n", + "\n", + " # Return the original string if the compressed string is not shorter\n", + " return compressed_str if len(compressed_str) < len(s) else s\n", + "\n", + "# Example usage\n", + "input_str = \"aabcccccaaa\"\n", + "\n", + "```\n", + "\n", + "## Proof of Correctness\n", + "\n", + "The algorithm for basic string compression is correct due to the following observations:\n", + "\n", + "1. The algorithm iterates through the input string once, keeping track of consecutive occurrences of each character.\n", + "2. It constructs a compressed string by appending each character and its count to a list.\n", + "3. The final compressed string is formed by joining the elements of the list.\n", + "\n", + "These steps guarantee that the compressed string accurately represents the counts of repeated characters in the original string. The correctness is ensured by the linear traversal of the input string and the careful handling of consecutive occurrences.\n", + "\n", + "## Complexity Analysis\n", + "\n", + "Let \\(n\\) be the length of the input string.\n", + "\n", + "### Time Complexity\n", + "\n", + "The time complexity of the algorithm is \\(O(n)\\) because it iterates through the input string once. Each character is processed once, and the processing involves constant-time operations.\n", + "\n", + "### Space Complexity\n", + "\n", + "The space complexity is \\(O(n)\\) in the worst case. The algorithm uses a list to store the compressed string, and the length of this list is proportional to the length of the input string. In the worst case, where there are no repeated characters, the compressed string could be as long as twice the length of the original string. Therefore, the space complexity can be considered \\(O(n)\\).\n", + "\n", + "The overall efficiency of the algorithm is linear with respect to the length of the input string, making it a practical and efficient solution for basic string compression.\n" + ], + "metadata": { + "id": "FzfjcITIr13b" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 10. Implement a trie (prefix tree) data structure to efficiently store a dictionary of words. Extend it to support autocomplete functionality.\n", + "\n", + "\n", + "```python\n", + "class TrieNode:\n", + " def __init__(self):\n", + " self.children = {}\n", + " self.is_end_of_word = False\n", + "\n", + "class Trie:\n", + " def __init__(self):\n", + " self.root = TrieNode()\n", + "\n", + " def insert(self, word):\n", + " node = self.root\n", + " for char in word:\n", + " if char not in node.children:\n", + " node.children[char] = TrieNode()\n", + " node = node.children[char]\n", + " node.is_end_of_word = True\n", + "\n", + " def search(self, word):\n", + " node = self.root\n", + " for char in word:\n", + " if char not in node.children:\n", + " return False\n", + " node = node.children[char]\n", + " return node.is_end_of_word\n", + "\n", + " def starts_with_prefix(self, prefix):\n", + " node = self.root\n", + " for char in prefix:\n", + " if char not in node.children:\n", + " return False\n", + " node = node.children[char]\n", + " return True\n", + "\n", + " def autocomplete(self, prefix):\n", + " node = self.root\n", + " for char in prefix:\n", + " if char not in node.children:\n", + " return []\n", + " node = node.children[char]\n", + " \n", + " suggestions = []\n", + " self._collect_words(node, prefix, suggestions)\n", + " return suggestions\n", + "\n", + " def _collect_words(self, node, current_prefix, suggestions):\n", + " if node.is_end_of_word:\n", + " suggestions.append(current_prefix)\n", + " \n", + " for char, child_node in node.children.items():\n", + " self._collect_words(child_node, current_prefix + char, suggestions)\n", + "\n", + "\n", + "# Example Usage\n", + "trie = Trie()\n", + "word_list = [\"apple\", \"app\", \"apricot\", \"banana\", \"bat\", \"batman\"]\n", + "for word in word_list:\n", + " trie.insert(word)\n", + "\n", + "prefix = \"ap\"\n", + "print(\"Autocomplete suggestions for prefix '{}': {}\".format(prefix, trie.autocomplete(prefix)))\n", + "\n", + "```\n", + "\n", + "## Proof of Correctness\n", + "\n", + "The correctness of the trie implementation can be proven by induction on the length of the words inserted into the trie:\n", + "\n", + "### Base Case\n", + "- For an empty trie, the `insert` method correctly creates the root node.\n", + "\n", + "### Inductive Step\n", + "- Assume that the `insert` method correctly inserts words of length `k` into the trie.\n", + "- When inserting a word of length `k + 1`, the method adds each character as a node in the trie, preserving the correctness of previously inserted words.\n", + "\n", + "The `search` method correctly determines the presence of a word by traversing the trie, and the `autocomplete` method collects all words with a given prefix, ensuring correct autocomplete functionality.\n", + "\n", + "## Complexity Analysis\n", + "\n", + "### Time Complexity\n", + "\n", + "- **Insertion Time Complexity (per word)**: O(n), where n is the length of the word being inserted.\n", + "- **Search Time Complexity (per word)**: O(n), where n is the length of the word being searched.\n", + "- **Autocomplete Time Complexity (per prefix)**: O(k + m), where k is the length of the prefix and m is the total number of nodes in the subtree rooted at the prefix.\n", + "\n", + "### Space Complexity\n", + "\n", + "- **Space Complexity (total)**: O(m), where m is the total number of characters in all words in the trie.\n", + "\n" + ], + "metadata": { + "id": "cBXaNAntsJ2-" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "FQ0hEjMWpMxu" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "DilTA9_YpOWS" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## **My ChatGPT Journey in Crafting an Algorithmic Problem**\n", + "\n", + "In this assignment, I took the journey of designing an algorithmic problem with the assistance of ChatGPT. Here's how it all unfolded, in a casual tone:\n", + "\n", + "1. **Getting Clarity on the Example Problem:** I kicked off by diving into the example problem given. ChatGPT played the role of my \"problem interpreter.\" It helped me grasp the nitty-gritty details of the example and explained how the solution was working.\n", + "\n", + "2. **Brainstorming for a New Problem:** ChatGPT became my brainstorming buddy. We had a chat (literally!) about ideas to create a fresh algorithmic problem. The goal was to craft a problem that would capture the essence of the example provided.\n", + "\n", + "3. **Crafting the New Problem:** Armed with ChatGPT's suggestions and insights, I began crafting a new problem statement. I made sure it included all the essentials: a clear problem description, the expected input-output formats, sample cases, and the constraints.\n", + "\n", + "4. **Coding with ChatGPT's Guidance:** Coding came next. I implemented a Python solution, and ChatGPT was right there, offering guidance and support. It provided coding tips, suggestions, and even helped me document the code properly.\n", + "\n", + "5. **Markdown Code:** To present everything neatly in a Jupyter Notebook, I used ChatGPT's assistance again. It helped me format the problem statement, code, and explanations into Markdown, making it all look clean and organized.\n", + "\n", + "6. **Reflecting on the Journey:** Lastly, I took a moment to reflect on how ChatGPT made the entire process smoother. It wasn't just a tool; it felt more like a helpful companion that made it easier for me to design a challenging problem and offered valuable support at every step.\n", + "\n", + "So, that's how ChatGPT played a crucial role in this assignment, making the journey of creating and analyzing algorithmic problems a breeze.\n" + ], + "metadata": { + "id": "1cBOxOT-j8fP" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "i96lN3D_lfe4" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/Submissions/002978981_Dev_Shah/Assignment-2/002978981_PSA_Assignment2.ipynb b/Submissions/002978981_Dev_Shah/Assignment-2/002978981_PSA_Assignment2.ipynb new file mode 100644 index 0000000..97a43af --- /dev/null +++ b/Submissions/002978981_Dev_Shah/Assignment-2/002978981_PSA_Assignment2.ipynb @@ -0,0 +1,823 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "## 1. Problem Statement - Maximal Network Rank\n", + "\n", + "You are given an infrastructure of `n` cities connected by roads. Each road connects two cities bidirectionally, and this is represented as an array `roads` where `roads[i] = [ai, bi]`.\n", + "\n", + "The **network rank** of two different cities is defined as the total number of directly connected roads to either city. If a road is directly connected to both cities, it is counted only once.\n", + "\n", + "Your task is to find the **maximal network rank** of the entire infrastructure, which is the maximum network rank among all pairs of different cities.\n", + "\n", + "Write a function `maximalNetworkRank(n, roads)` that takes an integer `n` (the number of cities) and a list of roads `roads`, and returns the maximal network rank.\n", + "\n", + "### Example 1\n", + "\n", + "```python\n", + "n = 4\n", + "roads = [[0,1],[0,3],[1,2],[1,3]]\n", + "\n", + "maximalNetworkRank(n, roads) # Output: 4\n" + ], + "metadata": { + "id": "XgJZOwwPK93E" + } + }, + { + "cell_type": "markdown", + "source": [ + "**Example 1:**\n", + "\n", + "![ex1.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASQAAACsCAYAAADSbdHtAAAFSnRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjAtMDktMjJUMDElM0EzNiUzQTIzLjIxNVolMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChYMTElM0IlMjBMaW51eCUyMHg4Nl82NCklMjBBcHBsZVdlYktpdCUyRjUzNy4zNiUyMChLSFRNTCUyQyUyMGxpa2UlMjBHZWNrbyklMjBDaHJvbWUlMkY4NS4wLjQxODMuMTAyJTIwU2FmYXJpJTJGNTM3LjM2JTIyJTIwZXRhZyUzRCUyMmRlOGVtQzJmd01TMWRiQ3ptUkNpJTIyJTIwdmVyc2lvbiUzRCUyMjEzLjcuMyUyMiUyMHR5cGUlM0QlMjJnb29nbGUlMjIlM0UlM0NkaWFncmFtJTIwaWQlM0QlMjJfdGVMMldVNXpxdDhiVDBWckZ6OCUyMiUyMG5hbWUlM0QlMjJQYWdlLTElMjIlM0U3VmpiY3Rvd0VQMGFIanRqV3dhY3g0UkxNcDEycGxONkNVOFpGUzIycUd5NVFvREoxMWRHYTJ4alNraWhMZWxraGdmdDJWMWQ5dXdSZ2hicHhkbXRvbW4wWGpJUUxjOWhXWXYwVzU3biUyQnA3WHlqOE9XMXVrMiUyRlV0RUNyT01LZ0VSdndSRUhRUVhYQUc4MXFnbGxKb250YkJpVXdTbU9nYVJwV1NxM3JZVklyNnFpa05vUUdNSmxRMDBhJTJCYzZjaWlnZGN0OFR2Z1lWU3M3SGF1ckNlbVJUQ2VaQjVSSmxjVmlBeGFwS2VrMUhZVVp6MFFlZkdLdXRpODRTJTJCODI0MHBTUFF4Q2FNdjN2WDY3YXpQeHJQQnpjZFBQeDRlSGolMkIlMkZhZHRabGxRczhNQzRXYjB1S2dETUZBUk5xWFFrUTVsUU1TalJHeVVYQ1lOOEdjZFlaY3c3S1ZNRHVnYWNnZFpyWkpjdXREUlFwR09CM3FsTTlKREdYT1JOY2dkaUNacFBLRG93eXcyTURRbTd6a2sxZGlJVHNNaVFDNEZyejdXUzM3ZEVlZmtNeHRtVFFxck5ZUWhyUThEOGJXVEZFM2pmU0tkalBNMjZZcW5uY3FFbWNLQ1lSWDlTRllJJTJCRUVkc1hGN1l5Z0xJMmkzSUdMUmFtd0FGZ21xJTJCckhjaXhZWU90M0VsNTJhQXREJTJCakJkeEdDempOSGhEQ0NDNnY5eXJpR2tZcDNSUmlaVFJmWjVMT1U2dkNLYyUyRnlqdGpEU0FxS201MkR5bWZoU1lqVWJhakdGWiUyRlZFMmRrZUFsS1EzYVFFJTJGU1NOaFlKcnpXUG9MMHFMd2tmb2FoeVB4VFkyVm04ZXA2UVVUMG5LJTJGYzRRZTRqWVk5VVQyWDg5NVZMamxTdWYxSEtKUTNPM1NicHI4cXRLOWZ2WHBoeSUyRlFhTDVFV3glMkJBYzRJZjQlMkY1cVR6OUcxNnlZOGV5TGklMkJMMjVvTXg1WHh2ME0wemJHdWpBU1U3Yjdxakd1R21YU3hpcXlMdkZ4ZGZZckdsTSUyRlNHNjJYTGFzczNPTmRIWjYwUjRJczZyUDhwMkoyazlOWkUlMkZjbUdqVDE5dnpuUEQ4YzE1ZkRpZTBaZkFpMyUyRnhCZyUyRk05cFAlMkZmM3pudDRLJTJCOUE0eFolMkZzeTN1aTMlMkZMQ0dEbnclM0QlM0QlM0MlMkZkaWFncmFtJTNFJTNDJTJGbXhmaWxlJTNFi88LagAAIABJREFUeF7tXQlUVVeyLWYQFEWQQXEAQcUB0MSZmARNgkNwSGsnONJxSn5r2rh+ot0rifl/abqjZui/4kA7djBiYpw1NhJbSZwiMiggoMigDIIoKEFG/6riPz8ElPfuu/fd4dVZy9W2755z6ux9e/c5detUWTx69OgRcGMEGAFGQAEIWLAgKYAFNoERYAQIARYkfhEYAUZAMQiwICmGCjaEEWAEWJD4HWAEGAHFIMCCpBgq2BBGgBFgQeJ3gBFgBBSDAAuSYqhgQxgBRoAFid8BRoARUAwCLEiKoYINYQQYAZMKUnnVHSi5XwjlVWVQVVsJDQ31YGlpBQ42juDs4AJu7T3B2aEzs6JSBJhfdRKXm5sLaWlpkJeXB2VlZVBTUwO2trbg4uIC3bt3h4CAAOjRo4dJFie5IGUUJUNmcRJkl16FiqqyNhfVwcEFfFz7gr97EPTxCGzzeX5AXgSYX3nxFzr7gQMHAP+cOHEC8vPz2xwGhSk0NBTCw8Ppj1RNEkGqa6iDc9mxkJB7upkI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)\n", + "\n", + "\n", + "**Input:**\n", + "```python\n", + "n = 4\n", + "roads = [[0,1],[0,3],[1,2],[1,3]]\n", + "```\n", + "\n", + "**Output:**\n", + "```python\n", + "4\n", + "```\n", + "\n", + "**Explanation:**\n", + "\n", + "Consider cities 0 and 1. Their network rank is 4 since there are 4 roads directly connected to either city. Importantly, the road connecting cities 0 and 1 is only counted once." + ], + "metadata": { + "id": "NTwvnWt-OC8T" + } + }, + { + "cell_type": "markdown", + "source": [ + "### Example 2\n", + "![ex2.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASQAAACsCAYAAADSbdHtAAAFkHRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjAtMDktMjJUMDElM0EzNiUzQTQxLjg3MVolMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChYMTElM0IlMjBMaW51eCUyMHg4Nl82NCklMjBBcHBsZVdlYktpdCUyRjUzNy4zNiUyMChLSFRNTCUyQyUyMGxpa2UlMjBHZWNrbyklMjBDaHJvbWUlMkY4NS4wLjQxODMuMTAyJTIwU2FmYXJpJTJGNTM3LjM2JTIyJTIwZXRhZyUzRCUyMmxUbkVUV3VreGdzaWYxbTkyWHVIJTIyJTIwdmVyc2lvbiUzRCUyMjEzLjcuMyUyMiUyMHR5cGUlM0QlMjJnb29nbGUlMjIlM0UlM0NkaWFncmFtJTIwaWQlM0QlMjJfdGVMMldVNXpxdDhiVDBWckZ6OCUyMiUyMG5hbWUlM0QlMjJQYWdlLTElMjIlM0U3VmpiYnVJd0VQMmFQSzZVSzRUSExvV3Q5aUpWNmtQYnA1V0xoOFJkRTBmR2xMQmZ2dzZaM0RDMEtSU1JTaXZ4NERrZVgyYk9ITnZCOHNhTDdKc2thZnhMVU9DV2E5UE04cTR0MTNWODE3WHluMDAzQlRJc2dVZ3lpazQxY01mJTJCQW9JMm9pdEdZZGx5VkVKd3hkSTJPQk5KQWpQVndvaVVZdDEybXd2ZVhqVWxFUmpBM1l4d0U3MW5WTVVGR3JyREdyOEJGc1hseXM1Z1ZQUXNTT21Na1N4alFzVzZBWGtUeXh0TElWVFJXbVJqNEhueXlyd1U0NllIZXF1TlNVaFVsd0h6aDk5TE9mcCUyQmV6V1pQQkY1VDUlMkZGODQ4dk9Nc0w0U3NNR0Rlck5tVUdnT3FFb0Nta2lrVWtFc0luTmZwVmlsVkNJViUyRkcxbGJ0ODFPSVZJT09CcDlCcVEyeVMxWkthQ2hXQzQ2OWM1R29LVmt3bmhmSkRmQVhVR3hHc0FOSE9hRzJJYUZYT2FuYVRrUUNCVEpsbk9QYVN5WEZuNG9vTjU5QmQ0NEZGM0liakVjRENLbGZlVFo2UXZmSkd3eDBUNUdBUE9wV29wZGlKV2Z3U2lveGM0cklDTlFyZnI1Sm5GT1ZnOVlSaUFVb3VkRXVFamhSN0tXOUQ0SUZIVlYlMkJOZWU2Z2JTJTJGb3dSY293UnNzd1k0MTRMTDg3Mk9tWUs3bEd4VHNkYWFiek5KbG1taHdqbkw4b3JZdzBnS2t1bWRnOHhuWVVtRTFHMnB4aFhmVlJPNGZaQUtNdXVRTkE1a0dBZDRBWWFNaDVRZm9yMnVKZThqRkRmVVhtSWZ6b24zUGxtaUZrN1dZVGQ1N1JQTkh1RjE1ZSUyQlFRbyUyRlZvZDlSaDBIUGRPZ2JuRHNtNlQzVzRiRW43VW5LOVljOVU2NWprbmE2TERPbUhocnRSNXdxYjE5blRXTlRHb21PNWFGcFBGWVMxMFk5YUd2Vm95NTJ1eHFNZHhCNjBGSG80U1ZWSFJpcTlqNlZxcyUyQmcwV0IwWVkwTzNyNWQlMkIlMkZ5azNYY2NPRWNjQiUyRllieDhHNUg4Sm51SUJ4NksxZ2VzYTZBTzJkQW5SMktxc0lDVWMxUDZGMkpncmVtcWlJMlpob1c2VlZQTWNYN3ZEJTJGcyUyRkRvcWd3N1ZtWGZQcyUyRktFcnZ3SiUyRm9uZXh0MFpYdDBTV3BEZzlrOTFQYjRiWENSRjM4UTl1ekZQekpZOUQ4VmkyZmc1SXd2UEczV2Y1a1c5MnI5eDdNMyUyQlFjJTNEJTNDJTJGZGlhZ3JhbSUzRSUzQyUyRm14ZmlsZSUzRQuZ+j4AACAASURBVHhe7V0HdFXF1t6EEgKaKISqdIkYVECkSBGUoiBVnyK9KFWkiTyKUiyAwEMFJUgVESS+X6mCCKiPJlVAHggBkSYQQJAmQgL869t5JwZIuOfMOeeecveslUUuOTOzZ39zvjuzZ8/ema5du3aNpIgGRAOiARdoIJMQkgtQEBFEA6IB1oAQkkwE0YBowDUaEEJyDRQiiGhANCCEJHNANCAacI0GhJBcA4UIIhoQDQghyRwQDYgGXKMBISTXQCGCiAZEA0JIMgdEA6IB12hACMk1UIggogHRQFAJ6cCBA7Rz5046ePAgnTp1ii5fvkzZsmWjXLlyUeHChSk2NpaKFCkiqHhUA2cu/k4nzh2lMxdP0cWkC3T16hUKC8tMEVlzUlRELspzewGKisjt0dH5V2w34WY7Ic2fP5/ws3z5cjp06FBAVEFM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)\n", + "\n", + "**Input:**\n", + "```python\n", + "n = 5\n", + "roads = [[0,1],[0,3],[1,2],[1,3],[2,3],[2,4]]\n", + "```\n", + "\n", + "**Output:**\n", + "```python\n", + "5\n", + "```\n", + "\n", + "**Explanation:**\n", + "\n", + "Consider cities 1 and 2. Their network rank is 5 since there are 5 roads directly connected to either city. Importantly, the road connecting cities 1 and 2 is only counted once." + ], + "metadata": { + "id": "Hm393Vu-Pcym" + } + }, + { + "cell_type": "markdown", + "source": [ + "### Constraints\n", + "\n", + "- `2 <= n <= 100`\n", + "- `0 <= roads.length <= n * (n - 1) / 2`\n", + "- `roads[i].length == 2`\n", + "- `0 <= ai, bi <= n-1`\n", + "- `ai != bi`\n", + "- Each pair of cities has at most one road connecting them.\n" + ], + "metadata": { + "id": "5k8ATFgCSmsf" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Psuedo Code\n", + "\n", + "```python\n", + "Function maximalNetworkRank(n, roads):\n", + " Initialize connections as an array of size n, all initialized to 0.\n", + " Initialize graph as a 2D array of size n x n, all initialized to False.\n", + " \n", + " For each road in roads:\n", + " Extract cities 'a' and 'b' from the road.\n", + " Increment connections[a] by 1 # Increase the connection count for city 'a'.\n", + " Increment connections[b] by 1 # Increase the connection count for city 'b'.\n", + " Set graph[a][b] = graph[b][a] = True # Mark the road as connected in both directions.\n", + " \n", + " Initialize max_rank as 0 # Variable to store the maximum network rank.\n", + " \n", + " For i from 0 to n - 1:\n", + " For j from i + 1 to n - 1:\n", + " Initialize rank as connections[i] + connections[j] # Calculate the initial rank.\n", + " \n", + " If graph[i][j] is True: # Check if there's a direct road between city 'i' and 'j'.\n", + " Decrement rank by 1 # Decrement the rank due to the shared road.\n", + " \n", + " Update max_rank as the maximum of max_rank and rank.\n", + " \n", + " Return max_rank # Return the maximal network rank.\n", + "\n", + "Call maximalNetworkRank(n, roads) to find the maximal network rank.\n", + "\n", + "```" + ], + "metadata": { + "id": "drcmH_VehLFA" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Solution" + ], + "metadata": { + "id": "nEsQIIs7SwZE" + } + }, + { + "cell_type": "code", + "source": [ + "from typing import List\n", + "\n", + "class Solution:\n", + " def maximalNetworkRank(self, n: int, roads: List[List[int]]) -> int:\n", + " # Initialize a list to keep track of the number of connections for each city.\n", + " connections = [0] * n\n", + "\n", + " # Initialize a graph as a 2D array where each cell represents a road connection.\n", + " graph = [[False] * n for _ in range(n)]\n", + "\n", + " # Iterate through the roads and update connections and the graph.\n", + " for a, b in roads:\n", + " connections[a] += 1 # Increase the connection count for city 'a'.\n", + " connections[b] += 1 # Increase the connection count for city 'b'.\n", + " graph[a][b] = graph[b][a] = True # Mark the road as connected in both directions.\n", + "\n", + " # Initialize a variable to store the maximum network rank.\n", + " max_rank = 0\n", + "\n", + " # Iterate through all pairs of cities.\n", + " for i in range(n):\n", + " for j in range(i + 1, n):\n", + " rank = connections[i] + connections[j] # Calculate the initial rank.\n", + "\n", + " # If there's a direct road between city 'i' and city 'j', decrement the rank.\n", + " if graph[i][j]:\n", + " rank -= 1\n", + "\n", + " # Update the maximum network rank.\n", + " max_rank = max(max_rank, rank)\n", + "\n", + " return max_rank\n" + ], + "metadata": { + "id": "fS-aMNRSdA0t" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Test Cases\n", + "\n", + "### Test Case 1\n", + "```python\n", + "n = 8\n", + "roads = [[0,1],[1,2],[2,3],[2,4],[5,6],[5,7]]\n", + "```\n", + "\n", + "Result : Passed\n", + "\n", + "\n", + "### Test Case 2\n", + "```python\n", + "n = 5\n", + "roads = [[0,1],[0,3],[1,2],[1,3],[2,3],[2,4]]\n", + "```\n", + "\n", + "Result : Passed\n", + "\n", + "\n", + "### Test Case 3\n", + "```python\n", + "n = 4\n", + "roads = [[0,1],[0,3],[1,2],[1,3]]\n", + "```\n", + "\n", + "Result : Passed" + ], + "metadata": { + "id": "n5CbR8H-dS3F" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Solution Breakdown\n", + "\n", + "Okay, so let's talk about how we're gonna solve this problem. We want to find the biggest network rank in a bunch of connected cities with roads. Network rank is just a fancy way of saying how many roads are connected to a city.\n", + "\n", + "**Step 1: Setting Up**\n", + "We're gonna start by getting organized. We'll create two things:\n", + " - `connections`: A list to keep count of how many roads are connected to each city. We'll start with zeros for everyone.\n", + " - `graph`: A 2D list to show which cities have direct roads. At first, everyone is not connected (marked as `False`).\n", + "\n", + "**Step 2: Counting Connections**\n", + "Now, we'll go through all the roads we have and update our lists:\n", + " - For each road `[a, b]`, we'll add 1 to the connection count for both city `a` and `b`.\n", + " - We'll also mark `graph[a][b]` and `graph[b][a]` as `True` to show they have a direct road between them.\n", + "\n", + "**Step 3: Find the Max Rank**\n", + "Here comes the fun part. We'll search for the highest network rank among all pairs of cities. We'll do this by:\n", + " - Going through every pair of cities, like `i` and `j` where `i` is less than `j`.\n", + " - We'll calculate an initial rank by adding up the connections for both cities, so `connections[i] + connections[j]`.\n", + " - If there's a direct road between city `i` and `j`, we'll subtract 1 to avoid double-counting.\n", + " - We'll keep track of the biggest rank we've seen so far in a variable called `max_rank`.\n", + "\n", + "**Step 4: The Big Reveal**\n", + "Finally, we'll tell you the answer! We'll give you the `max_rank` we found, and that's the maximal network rank.\n", + "\n", + "## Why It Works\n", + "\n", + "We're pretty sure this solution works because:\n", + " - We carefully set up our data to keep track of connections and roads.\n", + " - We went through all possible pairs of cities and calculated the rank for each one.\n", + " - `max_rank` always stores the highest rank we've seen.\n", + " - So, when we return `max_rank`, it's gotta be the biggest network rank in the whole infrastructure.\n", + "\n", + "And that's it! We're done here.\n" + ], + "metadata": { + "id": "uJxY_lGbgEvx" + } + }, + { + "cell_type": "markdown", + "source": [ + "## How ChatGPT Came to the Rescue\n", + "\n", + "I had some help from ChatGPT, and here's how it went down:\n", + "\n", + "1. **Problem Understanding**: First off, I presented the problem to ChatGPT, and it helped me break it down. It explained what we're trying to do - finding the biggest network rank in a bunch of connected cities with roads.\n", + "\n", + "2. **Pseudocode to Code**: ChatGPT came to the rescue by helping me translate that pseudocode into simple code. It made things much easier to understand.\n", + "\n", + "3. **Solution Explanation**: ChatGPT also helped me explain the solution. It turned my technical info into plain English.\n", + "\n", + "So, long story short, ChatGPT was a handy tool for sure!\n" + ], + "metadata": { + "id": "BqVvXDuci2kk" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "rn91_pjhWWTD" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "0OkmSRhXWVqQ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# 2. Given a directed acyclic graph (DAG), implement a function to perform a topological sort.\n", + "\n", + "```python\n", + "from collections import defaultdict\n", + "\n", + "def topological_sort(graph):\n", + " result = []\n", + " visited = set()\n", + "\n", + " def dfs(node):\n", + " visited.add(node)\n", + " for neighbor in graph[node]:\n", + " if neighbor not in visited:\n", + " dfs(neighbor)\n", + " result.append(node)\n", + "\n", + " for node in graph:\n", + " if node not in visited:\n", + " dfs(node)\n", + "\n", + " return result[::-1]\n", + "\n", + "# Example\n", + "graph = {1: [2, 3], 2: [4], 3: [4], 4: []}\n", + "result = topological_sort(graph)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input: {1: [2, 3], 2: [4], 3: [4], 4: []}\n", + "Output: [1, 3, 2, 4]\n", + "\n", + "**Proof of Correctness:**\n", + "The algorithm visits each node, explores its neighbors, and appends it to the result. The reversed result is a valid topological ordering. The correctness follows from the properties of DAGs.\n", + "\n", + "**Complexity:**\n", + "Time: O(V + E) - where V is the number of vertices and E is the number of edges.\n", + "Space: O(V) - additional space for the visited set.\n" + ], + "metadata": { + "id": "M8Dg0xadWXEo" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 3.Implement a function to find the shortest path from a given source node to all other nodes in a weighted, directed graph using Dijkstra's algorithm.\n", + "\n", + "```python\n", + "import heapq\n", + "\n", + "def dijkstra(graph, source):\n", + " distances = {node: float('infinity') for node in graph}\n", + " distances[source] = 0\n", + " priority_queue = [(0, source)]\n", + "\n", + " while priority_queue:\n", + " current_distance, current_node = heapq.heappop(priority_queue)\n", + "\n", + " if current_distance > distances[current_node]:\n", + " continue\n", + "\n", + " for neighbor, weight in graph[current_node].items():\n", + " distance = current_distance + weight\n", + " if distance < distances[neighbor]:\n", + " distances[neighbor] = distance\n", + " heapq.heappush(priority_queue, (distance, neighbor))\n", + "\n", + " return distances\n", + "\n", + "# Example\n", + "graph = {1: {2: 1, 3: 4}, 2: {3: 2, 4: 5}, 3: {4: 1}, 4: {}}\n", + "result = dijkstra(graph, 1)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input: {1: {2: 1, 3: 4}, 2: {3: 2, 4: 5}, 3: {4: 1}, 4: {}}\n", + "Output: {1: 0, 2: 1, 3: 3, 4: 4}\n", + "\n", + "**Proof of Correctness:**\n", + "Dijkstra's algorithm always selects the node with the smallest tentative distance, ensuring the correctness of the result.\n", + "\n", + "**Complexity:**\n", + "Time: O((V + E) * log(V)) - where V is the number of vertices and E is the number of edges.\n", + "Space: O(V) - additional space for distances and the priority queue." + ], + "metadata": { + "id": "JyJ9-o3ucH9M" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 4. Given a connected, undirected graph, implement a function to find the minimum spanning tree using Kruskal's algorithm.\n", + "\n", + "```python\n", + "def kruskal(graph):\n", + " parent = {node: node for node in graph}\n", + "\n", + " def find_set(node):\n", + " if parent[node] != node:\n", + " parent[node] = find_set(parent[node])\n", + " return parent[node]\n", + "\n", + " def union(u, v):\n", + " root_u, root_v = find_set(u), find_set(v)\n", + " parent[root_u] = root_v\n", + "\n", + " edges = [(weight, u, v) for u, neighbors in graph.items() for v, weight in neighbors.items()]\n", + " edges.sort()\n", + "\n", + " mst = []\n", + " for weight, u, v in edges:\n", + " if find_set(u) != find_set(v):\n", + " union(u, v)\n", + " mst.append((u, v, weight))\n", + "\n", + " return mst\n", + "\n", + "# Example\n", + "graph = {1: {2: 1, 3: 4}, 2: {3: 2, 4: 5}, 3: {4: 1}, 4: {}}\n", + "result = kruskal(graph)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input: {1: {2: 1, 3: 4}, 2: {3: 2, 4: 5}, 3: {4: 1}, 4: {}}\n", + "Output: [(1, 2, 1), (3, 4, 1), (2, 3, 2)]\n", + "\n", + "**Proof of Correctness:**\n", + "Kruskal's algorithm selects the smallest edge at each step while avoiding cycles, ensuring that the result is a valid minimum spanning tree.\n", + "\n", + "**Complexity:**\n", + "Time: O(E * log(V)) - where E is the number of edges and V is the number of vertices.\n", + "Space: O(V) - additional space for the parent data structure.\n" + ], + "metadata": { + "id": "mn9e5tbLceNc" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 5. A manufacturing company has raw materials, manufacturing steps, and products, each with varying costs and sale prices. Implement a function to determine the process for the day that maximizes profit.\n", + "\n", + "```python\n", + "import heapq\n", + "\n", + "def max_profit_process(raw_materials, manufacturing_steps, products):\n", + " graph = {}\n", + " for material, cost in raw_materials.items():\n", + " graph['source'] = graph.get('source', {})\n", + " graph['source'][material] = cost\n", + "\n", + " for step, (material, product, cost) in enumerate(manufacturing_steps):\n", + " graph[material] = graph.get(material, {})\n", + " graph[material][product] = cost\n", + "\n", + " for product, sale_price in products.items():\n", + " graph[product] = graph.get(product, {})\n", + " graph[product]['sink'] = sale_price\n", + "\n", + " distances = {node: float('-infinity') for node in graph}\n", + " distances['source'] = 0\n", + " priority_queue = [(-float('infinity'), 'source')]\n", + "\n", + " while priority_queue:\n", + " current_distance, current_node = heapq.heappop(priority_queue)\n", + "\n", + " if current_distance < distances[current_node]:\n", + " continue\n", + "\n", + " for neighbor, weight in graph[current_node].items():\n", + " profit = distances[current_node] - weight\n", + " if profit > distances[neighbor]:\n", + " distances[neighbor] = profit\n", + " heapq.heappush(priority_queue, (-profit, neighbor))\n", + "\n", + " return distances['sink']\n", + "\n", + "# Example\n", + "raw_materials = {'A': 2, 'B': 3}\n", + "manufacturing_steps = [('A', 'C', 1), ('B', 'C', 2), ('C', 'D', 3)]\n", + "products = {'D': 10}\n", + "result = max_profit_process(raw_materials, manufacturing_steps, products)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input:\n", + "\n", + "Raw Materials: {'A': 2, 'B': 3}\n", + "Manufacturing Steps: [('A', 'C', 1), ('B', 'C', 2), ('C', 'D', 3)]\n", + "Products: {'D': 10}\n", + "Output: 8\n", + "\n", + "**Proof of Correctness:**\n", + "The algorithm uses a single-source shortest path approach to find the maximum profit, considering costs and sale prices at each step.\n", + "\n", + "**Complexity:**\n", + "Time: O((V + E) * log(V)) - where V is the number of vertices and E is the number of edges.\n", + "Space: O(V) - additional space for distances and the priority queue.\n", + "\n" + ], + "metadata": { + "id": "b1AtIsQVcyEO" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 6. Implement a function to find the shortest path from a given source node to a destination node in an unweighted, directed graph using Breadth-First Search (BFS).\n", + "\n", + "```python\n", + "from collections import deque\n", + "\n", + "def bfs_shortest_path(graph, source, destination):\n", + " visited = set()\n", + " queue = deque([(source, [])])\n", + "\n", + " while queue:\n", + " current_node, path = queue.popleft()\n", + "\n", + " if current_node in visited:\n", + " continue\n", + "\n", + " visited.add(current_node)\n", + " path = path + [current_node]\n", + "\n", + " if current_node == destination:\n", + " return path\n", + "\n", + " queue.extend((neighbor, path) for neighbor in graph[current_node] if neighbor not in visited)\n", + "\n", + " return None\n", + "\n", + "# Example\n", + "graph = {1: [2, 3], 2: [4], 3: [4], 4: []}\n", + "result = bfs_shortest_path(graph, 1, 4)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input: {1: [2, 3], 2: [4], 3: [4], 4: []}\n", + "Output: [1, 3, 4]\n", + "\n", + "**Proof of Correctness:**\n", + "BFS explores the graph level by level, ensuring that the first path found from the source to the destination is the shortest.\n", + "\n", + "**Complexity:**\n", + "Time: O(V + E) - where V is the number of vertices and E is the number of edges.\n", + "Space: O(V) - additional space for the visited set and queue." + ], + "metadata": { + "id": "GzcDNs__dBe6" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 7. Implement a function to determine whether a given node is reachable from another node in a directed graph using Depth-First Search (DFS).\n", + "\n", + "```python\n", + "def dfs_reachable(graph, start, target):\n", + " visited = set()\n", + "\n", + " def dfs(node):\n", + " visited.add(node)\n", + " for neighbor in graph[node]:\n", + " if neighbor == target:\n", + " return True\n", + " if neighbor not in visited:\n", + " if dfs(neighbor):\n", + " return True\n", + " return False\n", + "\n", + " return dfs(start)\n", + "\n", + "# Example\n", + "graph = {1: [2, 3], 2: [4], 3: [4], 4: []}\n", + "result = dfs_reachable(graph, 1, 4)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input: {1: [2, 3], 2: [4], 3: [4], 4: []}\n", + "Output: True\n", + "\n", + "**Proof of Correctness:**\n", + "DFS explores the graph until it finds the target node or exhausts all possible paths, ensuring correctness in determining reachability.\n", + "\n", + "**Complexity:**\n", + "Time: O(V + E) - where V is the number of vertices and E is the number of edges.\n", + "Space: O(V) - additional space for the visited set.\n", + "\n" + ], + "metadata": { + "id": "ZWCXiMT1dTzK" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 8. Extend Dijkstra's algorithm to handle graphs with negative weights and implement the solution.\n", + "\n", + "```python\n", + "import heapq\n", + "\n", + "def dijkstra_with_negative_weights(graph, source):\n", + " distances = {node: float('infinity') for node in graph}\n", + " distances[source] = 0\n", + " priority_queue = [(0, source)]\n", + "\n", + " while priority_queue:\n", + " current_distance, current_node = heapq.heappop(priority_queue)\n", + "\n", + " if current_distance > distances[current_node]:\n", + " continue\n", + "\n", + " for neighbor, weight in graph[current_node].items():\n", + " distance = current_distance + weight\n", + " if distance < distances[neighbor]:\n", + " distances[neighbor] = distance\n", + " heapq.heappush(priority_queue, (distance, neighbor))\n", + "\n", + " return distances\n", + "\n", + "# Example\n", + "graph = {1: {2: 1, 3: 4}, 2: {3: -2, 4: 5}, 3: {4: 1}, 4: {}}\n", + "result = dijkstra_with_negative_weights(graph, 1)\n", + "print(result)\n", + "```\n", + "\n", + "**Example:**\n", + "Input: {1: {2: 1, 3: 4}, 2: {3: -2, 4: 5}, 3: {4: 1}, 4: {}}\n", + "Output: {1: 0, 2: 1, 3: -1, 4: 0}\n", + "\n", + "**Proof of Correctness:**\n", + "This extension of Dijkstra's algorithm accounts for negative weights and still guarantees the correct shortest path.\n", + "\n", + "**Complexity:**\n", + "Time: O((V + E) * log(V)) - where V is the number of vertices and E is the number of edges.\n", + "Space: O(V) - additional space for distances and the priority queue.\n" + ], + "metadata": { + "id": "ga2VlLnCdgVC" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 9. Given an undirected graph and two nodes A and B, implement a function to find the shortest path from node A to node B using Breadth-First Search.\n", + "\n", + "```python\n", + "from collections import deque\n", + "\n", + "def bfs_shortest_path(graph, start, end):\n", + " queue = deque([(start, [start])])\n", + "\n", + " while queue:\n", + " current, path = queue.popleft()\n", + "\n", + " if current == end:\n", + " return path\n", + "\n", + " for neighbor in graph[current]:\n", + " if neighbor not in path:\n", + " queue.append((neighbor, path + [neighbor]))\n", + "\n", + " return None\n", + "\n", + "# Example:\n", + "graph = {'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F', 'G'], 'D': ['B'], 'E': ['B'], 'F': ['C'], 'G': ['C']}\n", + "result = bfs_shortest_path(graph, 'A', 'G')\n", + "print(result)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The algorithm explores nodes layer by layer, ensuring that the first path found is the shortest due to BFS properties.\n", + "\n", + "**Complexity:**\n", + "Time: O(V + E) - where V is the number of vertices and E is the number of edges.\n", + "\n" + ], + "metadata": { + "id": "pl3Jp7z5ds0e" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 10. Implement a function to perform topological sorting on a directed acyclic graph (DAG) represented as an adjacency list.\n", + "\n", + "\n", + "```python\n", + "from collections import defaultdict, deque\n", + "\n", + "def topological_sort(graph):\n", + " in_degree = defaultdict(int)\n", + " for node in graph:\n", + " for neighbor in graph[node]:\n", + " in_degree[neighbor] += 1\n", + "\n", + " queue = deque([node for node in graph if in_degree[node] == 0])\n", + " result = []\n", + "\n", + " while queue:\n", + " current = queue.popleft()\n", + " result.append(current)\n", + "\n", + " for neighbor in graph[current]:\n", + " in_degree[neighbor] -= 1\n", + " if in_degree[neighbor] == 0:\n", + " queue.append(neighbor)\n", + "\n", + " return result\n", + "\n", + "# Example:\n", + "graph = {'A': ['B', 'C'], 'B': ['D'], 'C': ['D'], 'D': []}\n", + "result = topological_sort(graph)\n", + "print(result)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The algorithm ensures that each node is processed after its dependencies, adhering to the definition of topological sorting.\n", + "\n", + "**Complexity:**\n", + "Time: O(V + E) - where V is the number of vertices and E is the number of edges." + ], + "metadata": { + "id": "69V3_soveHuU" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "7aiHVEBsWVed" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "7d0jVyYjWVS8" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "kAt3HQI8WVDH" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [], + "metadata": { + "id": "IWvJujGUWUh8" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Challenges Faced\n", + "\n", + "1. **Maintaining Clarity**: The main challenge was keeping the problem statement clear and understandable while simplifying it. Wanted to make sure anyone reading it could grasp the concept easily.\n", + "\n", + "2. **Balancing Technical details**: Striking the right balance between an math tone and maintaining technical details was a bit tricky.\n", + "\n", + "3. **Ensuring Clarity**: Making sure that the pseudocode and solution explanations were crystal clear was a priority.\n", + "\n", + "These challenges were tackled to ensure that the problem statement maintained the spirit of the example while being clear and approachable.\n" + ], + "metadata": { + "id": "cmwnb6U2jna8" + } + }, + { + "cell_type": "markdown", + "source": [ + "\\## Lessons on Problem Design in Algorithmic Tasks\n", + "\n", + "1. Clarity is Key\n", + "\n", + "2. Balancing Complexity\n", + "\n", + "3. Consistency is Vital\n", + "\n", + "4. Writing diverse Test Cases\n", + "\n", + "5. Documentating the problem statement\n", + "\n", + "6. Iterative Refinement\n", + "\n", + "These lessons highlight the importance of clear, balanced, and accessible problem design when working with algorithms.\n" + ], + "metadata": { + "id": "iZ90hs7nkMIQ" + } + } + ] +} \ No newline at end of file diff --git a/Submissions/002978981_Dev_Shah/Assignment-3/002978981_PSA_Assignment3.ipynb b/Submissions/002978981_Dev_Shah/Assignment-3/002978981_PSA_Assignment3.ipynb new file mode 100644 index 0000000..57bed43 --- /dev/null +++ b/Submissions/002978981_Dev_Shah/Assignment-3/002978981_PSA_Assignment3.ipynb @@ -0,0 +1,840 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "Name: Dev Shah\n", + "NUID: 002978981\n", + "\n", + "\n", + "# Bellman-Ford algorithm\n", + "\n", + "## Problem Description\n", + "\n", + "You are given a directed, weighted graph with `N` nodes and `M` edges. The graph can contain negative weight edges, and your task is to find the shortest distance from a specified source node to all other nodes using the Bellman-Ford algorithm.\n", + "\n", + "Write a function `bellmanFord` that takes in the following parameters:\n", + "\n", + "- `graph`: A list of tuples, where each tuple represents an edge `(u, v, w)` from node `u` to node `v` with weight `w` (1 <= u, v <= N, -10^5 <= w <= 10^5). It is guaranteed that the graph does not contain any negative weight cycles.\n", + "- `N`: An integer, the number of nodes in the graph (1 <= N <= 1000).\n", + "- `source`: An integer, the source node from which you need to find the shortest distances (1 <= source <= N).\n", + "\n", + "The function should return a list of integers, where the `i-th` element represents the shortest distance from the source node to node `i`. If there is no path from the source to a particular node, the distance should be represented as `inf`.\n", + "\n", + "## Input and Output Format\n", + "\n", + "### Input\n", + "\n", + "- `graph`: A list of tuples representing directed edges in the graph.\n", + "- `N`: An integer, the number of nodes in the graph.\n", + "- `source`: An integer, the source node.\n", + "\n", + "### Output\n", + "\n", + "- A list of integers representing the shortest distances from the source node to all other nodes.\n", + "\n", + "## Sample Inputs and Outputs\n", + "\n", + "### Sample Input 1\n", + "\n", + "```python\n", + "graph = [(1, 2, 3), (2, 3, -1), (3, 1, -2)]\n", + "N = 3\n", + "source = 1\n", + "bellmanFord(graph, N, source)\n" + ], + "metadata": { + "id": "6G1q1ACSpx88" + } + }, + { + "cell_type": "markdown", + "source": [ + "###Sample Output 1\n", + "\n", + "```python\n", + "[0, 3, 1]\n", + "```\n", + "\n", + "### Sample Input 2\n", + "\n", + "```python\n", + "graph = [(1, 2, 2), (2, 3, 1), (3, 4, 3), (4, 1, -5)]\n", + "N = 4\n", + "source = 2\n", + "bellmanFord(graph, N, source)\n", + "```\n", + "\n", + "###Sample Output 2\n", + "\n", + "```python\n", + "[inf, 0, 1, -5]\n", + "```" + ], + "metadata": { + "id": "z_L_ynAHqIWX" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Constraints\n", + "\n", + "- The number of nodes `N` is between 1 and 1000.\n", + "- The weight of edges `w` is within the range of -10^5 to 10^5.\n", + "- There are no negative weight cycles in the graph.\n", + "- The source node is a valid node in the graph." + ], + "metadata": { + "id": "7uzml3myr3_k" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Understanding the Problem:**\n", + "You're given a directed, weighted graph with nodes and edges. The goal is to find the shortest distance from a specified source node to all other nodes in the graph using the Bellman-Ford algorithm. This algorithm is particularly useful when dealing with graphs that may contain negative-weight edges, as it can handle such cases.\n", + "\n", + "### **Pseudocode:**\n", + "Let's break down the Bellman-Ford algorithm into pseudocode for a clearer understanding:\n", + "\n", + "Step 1: Initialize Distances\n", + "```python\n", + "distances = [inf, inf, ..., inf] # Initialize distances for all nodes to infinity\n", + "distances[source] = 0 # Set the distance of the source node to 0\n", + "```\n", + "\n", + "Step 2: Relax Edges N-1 Times\n", + "```python\n", + "for _ in range(N - 1):\n", + " for edge in graph:\n", + " u, v, w = edge # Extract edge information (source, destination, weight)\n", + " if distances[u] != inf and distances[u] + w < distances[v]:\n", + " distances[v] = distances[u] + w\n", + "```" + ], + "metadata": { + "id": "yEwwrVqesA37" + } + }, + { + "cell_type": "code", + "source": [ + "def bellmanFord(graph, N, source):\n", + " # Step 1: Initialize distances\n", + " distances = [float('inf')] * N\n", + " distances[source - 1] = 0\n", + "\n", + " # Step 2: Relax all edges N-1 times\n", + " for _ in range(N - 1):\n", + " for u, v, w in graph:\n", + " if distances[u - 1] != float('inf') and distances[u - 1] + w < distances[v - 1]:\n", + " distances[v - 1] = distances[u - 1] + w\n", + "\n", + " # Step 3: Check for negative weight cycles (Not needed in this problem)\n", + "\n", + " return distances\n" + ], + "metadata": { + "id": "akodiunlwUZl" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "graph = [(1, 2, 3), (2, 3, -1), (3, 1, -2)]\n", + "N = 3\n", + "source = 1\n", + "shortest_distances = bellmanFord(graph, N, source)\n", + "print(shortest_distances) # Output: [0, 3, 2]" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "SBiIowZ2wWSL", + "outputId": "ed0bd35d-f409-49d2-cb3c-32d1ba178779" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[0, 3, 2]\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "In the above example, we use the Bellman-Ford algorithm to find the shortest distances from the source node (1) to all other nodes in the given graph. The output demonstrates the shortest distances.\n", + "\n", + "Feel free to adapt this solution to your specific problem, and you'll be able to find the shortest distances from a source node to all other nodes, even when negative weights are involved." + ], + "metadata": { + "id": "bLXaxs1BwtOV" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Proof of Correctness\n", + "The Bellman-Ford algorithm works by iteratively relaxing edges to ensure that shorter paths are consistently propagated. If there are no negative weight cycles, the algorithm guarantees correct shortest distances.\n", + "\n", + "## Time Complexity\n", + "\n", + "The Bellman-Ford algorithm has a time complexity of O(N * M), where N is the number of nodes and M is the number of edges.\n", + "\n", + "## Potential Improvements\n", + "\n", + "1. **Early Termination**: Consider breaking out of the loop early if no distances are updated during an iteration.\n", + "\n", + "2. **Priority Queue**: You can optimize the algorithm by using a priority queue (e.g., a min-heap) to select the edge with the minimum distance to relax at each step. This can reduce the time complexity to O(N * log(N) + M) for sparse graphs.\n", + "\n", + "3. **Parallelization**: If dealing with a large graph and the graph can be divided into smaller subproblems, consider parallelizing the Bellman-Ford algorithm to improve computation speed.\n", + "\n", + "4. **Data Structures**: Ensure you use appropriate data structures for graph representation to optimize the time complexity of essential operations.\n", + "\n", + "By implementing these improvements, you can enhance the efficiency of the Bellman-Ford algorithm, making it more suitable for various scenarios.\n" + ], + "metadata": { + "id": "JUIHEvZEwwby" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "KNgG5vY0fbFq" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "Feb18YgGfa3a" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# 2. You are given a weighted directed graph represented by an adjacency list and a starting node s. Implement the Bellman-Ford algorithm to find the shortest paths from the starting node to all other nodes in the graph. If there is a negative-weight cycle in the graph, return an empty list.\n", + "\n", + "\n", + "```python\n", + "def bellman_ford(graph, s):\n", + " # Initialization\n", + " distance = {node: float('inf') for node in graph}\n", + " distance[s] = 0\n", + " \n", + " # Relax edges repeatedly\n", + " for _ in range(len(graph) - 1):\n", + " for node in graph:\n", + " for neighbor, weight in graph[node]:\n", + " if distance[node] + weight < distance[neighbor]:\n", + " distance[neighbor] = distance[node] + weight\n", + " \n", + " # Check for negative-weight cycles\n", + " for node in graph:\n", + " for neighbor, weight in graph[node]:\n", + " if distance[node] + weight < distance[neighbor]:\n", + " return []\n", + " \n", + " return distance\n", + "\n", + "# Example usage\n", + "graph = {\n", + " 'A': [('B', 1), ('C', 4)],\n", + " 'B': [('C', -2)],\n", + " 'C': [('D', 2)],\n", + " 'D': []\n", + "}\n", + "start_node = 'A'\n", + "result = bellman_ford(graph, start_node)\n", + "print(result)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The Bellman-Ford algorithm guarantees correctness by relaxing edges repeatedly for |V| - 1 times, where |V| is the number of vertices in the graph. If there is a negative-weight cycle, the algorithm detects it during the extra iteration and returns an empty list.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the Bellman-Ford algorithm is O(|V| * |E|), where |V| is the number of vertices and |E| is the number of edges in the graph." + ], + "metadata": { + "id": "j1pzPVEIfcdD" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 3. You are given a network flow problem represented by a graph and capacities on edges. Implement the Ford-Fulkerson algorithm to find the maximum flow from a source node s to a sink node t. Assume capacities are integers.\n", + "\n", + "\n", + "```python\n", + "def ford_fulkerson(graph, s, t):\n", + " # Initialization\n", + " residual_graph = graph.copy()\n", + " max_flow = 0\n", + " \n", + " while True:\n", + " # Find an augmenting path using DFS\n", + " augmenting_path = find_augmenting_path(residual_graph, s, t)\n", + " \n", + " if not augmenting_path:\n", + " break\n", + " \n", + " # Update the residual graph and augment the flow\n", + " residual_graph, flow = update_residual_graph(residual_graph, augmenting_path)\n", + " max_flow += flow\n", + " \n", + " return max_flow\n", + "\n", + "def find_augmenting_path(graph, s, t):\n", + " # DFS to find an augmenting path\n", + " stack = [(s, [s])]\n", + " \n", + " while stack:\n", + " current_node, path = stack.pop()\n", + " \n", + " for neighbor, capacity in graph[current_node]:\n", + " if neighbor not in path and capacity > 0:\n", + " if neighbor == t:\n", + " return path + [neighbor]\n", + " stack.append((neighbor, path + [neighbor]))\n", + " \n", + " return None\n", + "\n", + "def update_residual_graph(graph, path):\n", + " # Find the minimum capacity on the augmenting path\n", + " min_capacity = min(graph[path[i]][path[i+1]] for i in range(len(path) - 1))\n", + " \n", + " # Update capacities and reverse edges\n", + " for i in range(len(path) - 1):\n", + " graph[path[i]][path[i+1]] -= min_capacity\n", + " graph[path[i+1]][path[i]] += min_capacity\n", + " \n", + " return graph, min_capacity\n", + "\n", + "# Example usage\n", + "graph = {\n", + " 's': {'A': 10, 'B': 5},\n", + " 'A': {'B': 15, 't': 10},\n", + " 'B': {'t': 10},\n", + " 't': {}\n", + "}\n", + "source = 's'\n", + "sink = 't'\n", + "result = ford_fulkerson(graph, source, sink)\n", + "print(result)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The Ford-Fulkerson algorithm terminates when there are no more augmenting paths in the residual graph. It increases the flow along each augmenting path until no more paths can be found. The algorithm converges to the maximum flow.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the Ford-Fulkerson algorithm depends on the choice of augmenting paths. In the worst case, it can be O(E * |f*|), where E is the number of edges and |f*| is the maximum flow." + ], + "metadata": { + "id": "a35Pl38IfcUJ" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 4.You are given a network flow problem represented by a graph with capacities on edges. Implement the Preflow-Push algorithm to find the maximum flow from a source node s to a sink node t. The algorithm uses push and relabel operations with height labeling. Assume capacities are integers.\n", + "\n", + "```python\n", + "def preflow_push(graph, s, t):\n", + " # Initialization\n", + " heights = {node: 0 for node in graph}\n", + " excess = {node: 0 for node in graph}\n", + " heights[s] = len(graph)\n", + " excess[s] = float('inf')\n", + " \n", + " # Initialize preflow by saturating outgoing edges from the source\n", + " for neighbor, capacity in graph[s].items():\n", + " excess[neighbor] = capacity\n", + " excess[s] -= capacity\n", + " graph[neighbor][s] = capacity\n", + " \n", + " # Main loop\n", + " while True:\n", + " # Find a node with excess flow and perform push or relabel\n", + " found_push = False\n", + " for node in graph:\n", + " if node != s and node != t and excess[node] > 0:\n", + " if push(graph, heights, excess, node):\n", + " found_push = True\n", + " break\n", + " relabel(heights, excess, node)\n", + " \n", + " if not found_push:\n", + " break\n", + " \n", + " return excess[t]\n", + "\n", + "def push(graph, heights, excess, node):\n", + " # Perform push operation on the node\n", + " for neighbor, capacity in graph[node].items():\n", + " if heights[node] == heights[neighbor] + 1 and excess[node] > 0:\n", + " flow = min(excess[node], capacity)\n", + " graph[node][neighbor] -= flow\n", + " graph[neighbor][node] += flow\n", + " excess[node] -= flow\n", + " excess[neighbor] += flow\n", + " return True\n", + " return False\n", + "\n", + "def relabel(heights, excess, node):\n", + " # Perform relabel operation on the node\n", + " min_height = float('inf')\n", + " for neighbor, capacity in graph[node].items():\n", + " if capacity > 0:\n", + " min_height = min(min_height, heights[neighbor])\n", + " heights[node] = min_height + 1\n", + "\n", + "# Example usage\n", + "graph = {\n", + " 's': {'A': 10, 'B': 5},\n", + " 'A': {'B': 15, 't': 10},\n", + " 'B': {'t': 10},\n", + " 't': {}\n", + "}\n", + "source = 's'\n", + "sink = 't'\n", + "result = preflow_push(graph, source, sink)\n", + "print(result)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The Preflow-Push algorithm maintains the flow conservation property and the height labeling property during push and relabel operations, ensuring correctness.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the Preflow-Push algorithm is O(V^3), where V is the number of vertices in the graph." + ], + "metadata": { + "id": "fyh7QInPfcHZ" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 5. Given a flow network represented by a graph and capacities on edges, implement a function to find the minimum s-t cut in the network. Additionally, write a function to reduce the flow network capacity by deleting a specified edge.\n", + "\n", + "```python\n", + "def min_cut(graph, s, t):\n", + " # Implement the Ford-Fulkerson algorithm to find the maximum flow\n", + " # Use any available implementation, e.g., the one provided in Problem 2\n", + " max_flow = ford_fulkerson(graph, s, t)\n", + " \n", + " # Find the minimum s-t cut\n", + " visited = set()\n", + " stack = [s]\n", + " while stack:\n", + " current_node = stack.pop()\n", + " visited.add(current_node)\n", + " for neighbor, capacity in graph[current_node].items():\n", + " if neighbor not in visited and capacity > 0:\n", + " stack.append(neighbor)\n", + " \n", + " # Construct the minimum s-t cut\n", + " min_cut_edges = []\n", + " for node in visited:\n", + " for neighbor, capacity in graph[node].items():\n", + " if neighbor not in visited:\n", + " min_cut_edges.append((node, neighbor))\n", + " \n", + " return min_cut_edges\n", + "\n", + "def reduce_capacity(graph, edge):\n", + " # Delete the specified edge from the flow network\n", + " u, v = edge\n", + " del graph[u][v]\n", + " del graph[v][u]\n", + "\n", + "# Example usage\n", + "graph = {\n", + " 's': {'A': 10, 'B': 5},\n", + " 'A': {'B': 15, 't': 10},\n", + " 'B': {'t': 10},\n", + " 't': {}\n", + "}\n", + "source = 's'\n", + "sink = 't'\n", + "cut = min_cut(graph, source, sink)\n", + "print(\"Minimum s-t cut:\", cut)\n", + "\n", + "# Reduce the capacity of edge ('A', 'B')\n", + "reduce_capacity(graph, ('A', 'B'))\n", + "print(\"Updated graph:\", graph)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The min_cut function correctly identifies the minimum s-t cut by finding the set of nodes reachable from the source after the Ford-Fulkerson algorithm. The reduce_capacity function removes the specified edge, maintaining the correctness of the graph.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of min_cut is O(V + E) and reduce_capacity is O(1), where V is the number of vertices and E is the number of edges in the graph." + ], + "metadata": { + "id": "fvehZBwmfb_j" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 6. You are given a network flow problem representing the assignment of people to hospitals. Implement a function to balance the load by redistributing people while minimizing the total cost. The cost is determined by the distance from each person to their assigned hospital. Use the Preflow-Push algorithm for optimization.\n", + "\n", + "```python\n", + "def balance_load(graph):\n", + " # Add a source node and connect it to people nodes with capacities equal to their demand\n", + " source = 'source'\n", + " for person in graph:\n", + " graph[source][person] = graph[person]['demand']\n", + " \n", + " # Add a sink node and connect hospitals to it with capacities equal to their capacity\n", + " sink = 'sink'\n", + " for hospital in graph.keys():\n", + " if hospital != source:\n", + " graph[hospital][sink] = graph[hospital]['capacity']\n", + " \n", + " # Run Preflow-Push algorithm to find the maximum flow\n", + " max_flow = preflow_push(graph, source, sink)\n", + " \n", + " # Remove artificial edges connected to the source and the sink\n", + " del graph[source]\n", + " for node in graph:\n", + " del graph[node][sink]\n", + " \n", + " # Construct the balanced load\n", + " balanced_load = {}\n", + " for person in graph:\n", + " for hospital, capacity in graph[person].items():\n", + " if hospital != source and capacity == 0:\n", + " balanced_load[person] = hospital\n", + " \n", + " return balanced_load\n", + "\n", + "# Example usage\n", + "graph = {\n", + " 'Person1': {'Hospital1': 5, 'Hospital2': 3, 'demand': 5},\n", + " 'Person2': {'Hospital2': 2, 'Hospital3': 4, 'demand': 2},\n", + " 'Person3': {'Hospital1': 6, 'Hospital3': 1, 'demand': 3},\n", + " 'Hospital1': {'capacity': 8},\n", + " 'Hospital2': {'capacity': 5},\n", + " 'Hospital3': {'capacity': 6},\n", + " 'source': {},\n", + " 'sink': {}\n", + "}\n", + "result = balance_load(graph)\n", + "print(\"Balanced load:\", result)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The balance_load function constructs a flow network, runs the Preflow-Push algorithm to find the maximum flow, and then identifies the balanced load based on the flow in the network. The correctness relies on the correctness of the Preflow-Push algorithm.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the balance_load function is determined by the underlying Preflow-Push algorithm, which is O(V^3), where V is the number of vertices in the graph." + ], + "metadata": { + "id": "UURys0nTxfuz" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 7. You are given a set of locations where ATMs can be placed and the expected usage demands for each location. Implement a function to optimize the placement of ATMs to minimize the total cost, considering both the fixed cost of installing an ATM at a location and the transportation cost to meet the demand from different locations.\n", + "\n", + "```python\n", + "import itertools\n", + "\n", + "def atm_location_optimization(locations, demands, fixed_costs, transportation_costs):\n", + " min_cost = float('inf')\n", + " optimal_placement = None\n", + "\n", + " # Generate all possible ATM placements\n", + " all_placements = list(itertools.product([0, 1], repeat=len(locations)))\n", + "\n", + " # Iterate through all possible placements and calculate total cost\n", + " for placement in all_placements:\n", + " total_cost = calculate_total_cost(placement, demands, fixed_costs, transportation_costs)\n", + " if total_cost < min_cost:\n", + " min_cost = total_cost\n", + " optimal_placement = placement\n", + "\n", + " return optimal_placement\n", + "\n", + "def calculate_total_cost(placement, demands, fixed_costs, transportation_costs):\n", + " total_cost = 0\n", + "\n", + " # Calculate fixed costs for installed ATMs\n", + " for i in range(len(placement)):\n", + " if placement[i] == 1:\n", + " total_cost += fixed_costs[i]\n", + "\n", + " # Calculate transportation costs based on demand and placement\n", + " for i in range(len(demands)):\n", + " total_cost += demands[i] * sum(placement[j] * transportation_costs[j][i] for j in range(len(placement)))\n", + "\n", + " return total_cost\n", + "\n", + "# Example usage\n", + "locations = ['Location1', 'Location2', 'Location3']\n", + "demands = [10, 20, 15]\n", + "fixed_costs = [1000, 1500, 1200]\n", + "transportation_costs = [\n", + " [5, 8, 6],\n", + " [4, 7, 9],\n", + " [3, 6, 5]\n", + "]\n", + "\n", + "result = atm_location_optimization(locations, demands, fixed_costs, transportation_costs)\n", + "print(\"Optimal ATM Placement:\", result)\n", + "\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The atm_location_optimization function generates all possible ATM placements, calculates the total cost for each placement, and selects the placement with the minimum cost. The correctness depends on the accuracy of the cost calculation.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the atm_location_optimization function is O(2^n * n^2), where n is the number of locations. This is because it iterates through all possible placements (2^n) and calculates the total cost (n^2).\n" + ], + "metadata": { + "id": "D82gWXMDxfrc" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 8. You are given a recurrence relation of the form T(n) = a * T(n/b) + f(n), where a, b are constants and f(n) is an asymptotically positive function. Implement a function to analyze the time complexity using the Master Theorem.\n", + "\n", + "```python\n", + "def master_theorem_analysis(a, b, f_n):\n", + " # Determine the values of log(a, b) and compare with f(n)\n", + " log_ratio = math.log(a, b)\n", + " \n", + " if log_ratio > f_n:\n", + " return \"Time complexity: O(n^\" + str(log_ratio) + \")\"\n", + " elif log_ratio == f_n:\n", + " return \"Time complexity: O(n^\" + str(log_ratio) + \" * log n)\"\n", + " else:\n", + " return \"Time complexity: O(\" + f_n + \")\"\n", + "\n", + "# Example usage\n", + "a = 3\n", + "b = 2\n", + "f_n = 2\n", + "result = master_theorem_analysis(a, b, f_n)\n", + "print(result)\n", + "\n", + "```\n", + "**Proof of Correctness:**\n", + "The master_theorem_analysis function applies the Master Theorem to determine the time complexity based on the given recurrence relation. The correctness relies on the correct application of the Master Theorem.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the master_theorem_analysis function is O(1), as it performs a constant number of operations.\n" + ], + "metadata": { + "id": "6XIlN6H2xfm8" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 9. Implement a function to find the length of the longest increasing subsequence in a given array of integers. The function should return the length of the subsequence and the subsequence itself.\n", + "\n", + "```python\n", + "def longest_increasing_subsequence(nums):\n", + " if not nums:\n", + " return 0, []\n", + "\n", + " n = len(nums)\n", + " lengths = [1] * n\n", + " sequences = [[num] for num in nums]\n", + "\n", + " for i in range(1, n):\n", + " for j in range(i):\n", + " if nums[i] > nums[j] and lengths[i] < lengths[j] + 1:\n", + " lengths[i] = lengths[j] + 1\n", + " sequences[i] = sequences[j] + [nums[i]]\n", + "\n", + " max_length = max(lengths)\n", + " max_length_index = lengths.index(max_length)\n", + " return max_length, sequences[max_length_index]\n", + "\n", + "# Example usage\n", + "nums = [10, 22, 9, 33, 21, 50, 41, 60, 80]\n", + "result_length, result_sequence = longest_increasing_subsequence(nums)\n", + "print(\"Length of Longest Increasing Subsequence:\", result_length)\n", + "print(\"Longest Increasing Subsequence:\", result_sequence)\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The longest_increasing_subsequence function uses dynamic programming to find the length and the actual subsequence of the longest increasing subsequence in the given array. The correctness is evident from the fact that the function considers all possible subsequences and returns the longest one.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the longest_increasing_subsequence function is O(n^2), where n is the length of the input array. This is because the function iterates through all pairs of elements in the array.\n" + ], + "metadata": { + "id": "bCUfTm1Oxfd3" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 10. Given a set of positive integers and a target sum, implement a function to determine whether there exists a subset of the given set that adds up to the target sum.\n", + "\n", + "\n", + "```python\n", + "def subset_sum_exists(nums, target_sum):\n", + " # Create a 2D array to store the subset sum possibilities\n", + " n = len(nums)\n", + " dp = [[False] * (target_sum + 1) for _ in range(n + 1)]\n", + "\n", + " # Base case: an empty subset can always achieve a sum of 0\n", + " for i in range(n + 1):\n", + " dp[i][0] = True\n", + "\n", + " # Fill the DP array to check subset sum possibilities\n", + " for i in range(1, n + 1):\n", + " for j in range(1, target_sum + 1):\n", + " if nums[i - 1] <= j:\n", + " dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]\n", + " else:\n", + " dp[i][j] = dp[i - 1][j]\n", + "\n", + " # The final cell of the DP array indicates whether the target sum is achievable\n", + " return dp[n][target_sum]\n", + "\n", + "# Example usage\n", + "nums = [3, 34, 4, 12, 5, 2]\n", + "target_sum = 9\n", + "result = subset_sum_exists(nums, target_sum)\n", + "print(\"Subset with sum\", target_sum, \"exists:\", result)\n", + "\n", + "```\n", + "\n", + "**Proof of Correctness:**\n", + "The subset_sum_exists function uses dynamic programming to determine whether there exists a subset of the given set that adds up to the target sum. The correctness is based on the correct filling of the DP array, where each cell represents whether a particular sum can be achieved with the current subset.\n", + "\n", + "**Time Complexity:**\n", + "The time complexity of the subset_sum_exists function is O(n * target_sum), where n is the length of the input array and target_sum is the given target sum. The function iterates through all possible sums for each element in the array." + ], + "metadata": { + "id": "SjqyMBVKfb3P" + } + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "KXm7og-gfasj" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "xdC4NEjFfagC" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## How ChatGPT Helped Me Solve the Problem\n", + "\n", + "As I approached the problem of finding the shortest distances in a weighted graph using the Bellman-Ford algorithm, I found the assistance of ChatGPT invaluable. Here's how ChatGPT played a crucial role in helping me tackle the problem:\n", + "\n", + "1. **Problem Understanding**: ChatGPT provided a clear problem statement with well-structured inputs, outputs, sample examples, and constraints. This made it easier for me to understand the problem's requirements.\n", + "\n", + "2. **Algorithm Explanation**: ChatGPT explained the Bellman-Ford algorithm in detail, providing pseudocode and a written explanation. This helped me grasp the algorithm's inner workings and how it could be applied to my problem.\n", + "\n", + "3. **Python Implementation**: ChatGPT not only described the algorithm but also offered a Python implementation with well-documented code. This saved me a significant amount of time that I would have spent on coding and debugging.\n", + "\n", + "4. **Proof of Correctness**: ChatGPT provided a proof of correctness for the solution, assuring me that the algorithm would produce accurate results. It made me confident in the approach.\n", + "\n", + "5. **Pseudocode Guidance**: The pseudocode offered by ChatGPT was instrumental in planning and structuring my code. It served as a foundation for my Python implementation.\n", + "\n", + "6. **Time Complexity and Improvements**: ChatGPT explained the time complexity of the algorithm and suggested potential improvements. This information helped me optimize my solution and consider enhancements for efficiency.\n", + "\n", + "Overall, ChatGPT acted as a knowledgeable and reliable guide throughout my problem-solving journey. It not only equipped me with the technical details and code but also offered insights to make my solution more efficient. With ChatGPT's assistance, I successfully implemented the Bellman-Ford algorithm and solved the problem effectively.\n" + ], + "metadata": { + "id": "tpcs6QcrxxMF" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Challenges Faced\n", + "\n", + "While working on the problem of finding the shortest distances in a weighted graph using the Bellman-Ford algorithm, I encountered several challenges that required careful consideration and problem-solving. Here are the main challenges I faced:\n", + "\n", + "1. **Algorithm Complexity**: The Bellman-Ford algorithm has a time complexity of O(N * M), which can be computationally expensive for large graphs. Understanding the algorithm's complexity and the potential need for optimization was a challenge.\n", + "\n", + "2. **Negative Weight Edges**: Dealing with graphs that contain negative weight edges added complexity. I had to ensure that the algorithm handled these cases correctly and that it wouldn't result in incorrect distances.\n", + "\n", + "3. **Algorithm Optimization**: Optimizing the algorithm for efficiency was a challenge. I had to explore techniques like early termination, priority queues, and parallelization to make the algorithm more scalable.\n", + "\n", + "4. **Graph Representation**: Choosing the right data structures and representing the graph effectively was a challenge. An improper choice of data structures could lead to slower execution and memory issues.\n", + "\n", + "5. **Correctness Assurance**: Ensuring the correctness of the algorithm was crucial. I needed to understand and trust that the Bellman-Ford algorithm would produce accurate results.\n", + "\n", + "6. **Implementation**: Translating the pseudocode and algorithm explanation into a working Python implementation required careful attention to detail and debugging.\n", + "\n", + "Despite these challenges, the guidance and explanations provided by ChatGPT were instrumental in overcoming them and successfully implementing the Bellman-Ford algorithm to solve the problem.\n" + ], + "metadata": { + "id": "S-brzYjGx7x5" + } + }, + { + "cell_type": "markdown", + "source": [ + "\\## Lessons on Problem Design in Algorithmic Tasks\n", + "\n", + "1. Clarity is Key\n", + "\n", + "2. Balancing Complexity\n", + "\n", + "3. Consistency is Vital\n", + "\n", + "4. Writing diverse Test Cases\n", + "\n", + "5. Documentating the problem statement\n", + "\n", + "6. Iterative Refinement\n", + "\n", + "These lessons highlight the importance of clear, balanced, and accessible problem design when working with algorithms." + ], + "metadata": { + "id": "Xs001xOCx9OT" + } + } + ] +} \ No newline at end of file diff --git a/Submissions/002978981_Dev_Shah/Assignment-4/002978981_PSA_Assignment4.ipynb b/Submissions/002978981_Dev_Shah/Assignment-4/002978981_PSA_Assignment4.ipynb new file mode 100644 index 0000000..0af102c --- /dev/null +++ b/Submissions/002978981_Dev_Shah/Assignment-4/002978981_PSA_Assignment4.ipynb @@ -0,0 +1,1106 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# **1. Vertex-Disjoint Cycle-Cover Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "You are given a directed graph. The goal is to find a minimum number of vertex-disjoint cycles that cover all vertices in the graph.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of vertices in the graph.\n", + "- An integer `m` representing the number of edges in the graph.\n", + "- `m` lines containing two integers `u` and `v` (1 <= u, v <= n), representing a directed edge from vertex `u` to vertex `v`.\n", + "\n", + "### Output Format:\n", + "\n", + "An integer representing the minimum number of vertex-disjoint cycles needed to cover all vertices. If no such cycle-cover exists, return -1.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "5 6\n", + "1 2\n", + "2 3\n", + "3 1\n", + "3 4\n", + "4 5\n", + "5 3\n", + "```\n", + "\n", + "### Sample Output:\n", + "\n", + "```plaintext\n", + "2\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n <= 1000\n", + "- 1 <= m <= 5000\n", + "\n", + "### Solution:\n", + "```python\n", + " def find_vertex_disjoint_cycle_cover(n, m, edges):\n", + " # Create an adjacency list\n", + " adjacency_list = [[] for _ in range(n)]\n", + " for u, v in edges:\n", + " adjacency_list[u].append(v)\n", + "\n", + " # Check if the graph is connected\n", + " visited = [False] * n\n", + " def dfs(u):\n", + " visited[u] = True\n", + " for v in adjacency_list[u]:\n", + " if not visited[v]:\n", + " dfs(v)\n", + "\n", + " dfs(0)\n", + " if not all(visited):\n", + " return -1\n", + "\n", + " # Create a residual graph\n", + " residual_adjacency_list = [[] for _ in range(n)]\n", + " for u, v in edges:\n", + " if not visited[v]:\n", + " residual_adjacency_list[u].append(v)\n", + "\n", + " # Find a maximum matching in the residual graph\n", + " matching = []\n", + " def find_augmenting_path(u):\n", + " if u == -1:\n", + " return True\n", + "\n", + " for v in residual_adjacency_list[u]:\n", + " if visited[v]:\n", + " continue\n", + "\n", + " visited[v] = True\n", + " if find_augmenting_path(v):\n", + " matching.append((u, v))\n", + " return True\n", + "\n", + " return False\n", + "\n", + " visited = [False] * n\n", + " while find_augmenting_path(0):\n", + " visited = [False] * n\n", + "\n", + " # Count the number of cycles\n", + " cycles = len(matching) // 2\n", + "\n", + " return cycles\n", + "\n", + "# Read the input\n", + "n, m = map(int, input().split())\n", + "edges = []\n", + "for _ in range(m):\n", + " u, v = map(int, input().split())\n", + " edges.append((u, v))\n", + "\n", + "# Find the minimum number of vertex-disjoint cycles\n", + "cycles = find_vertex_disjoint_cycle_cover(n, m, edges)\n", + "\n", + "# Print the output\n", + "print(cycles)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by showing that the algorithm finds a set of vertex-disjoint cycles that cover all vertices. The cycles can be validated to ensure they are indeed disjoint and cover all vertices.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V + E)\n", + "- Space Complexity: O(V + E)" + ], + "metadata": { + "id": "q08jNnLF716X" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **2. Directed Disjoint Paths Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given a directed graph and two vertices `s` and `t`, determine if there exist two vertex-disjoint paths from `s` to `t`.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of vertices in the graph.\n", + "- An integer `m` representing the number of edges in the graph.\n", + "- `m` lines containing two integers `u` and `v` (1 <= u, v <= n), representing a directed edge from vertex `u` to vertex `v`.\n", + "- Two integers `s` and `t` (1 <= s, t <= n), representing the source and target vertices.\n", + "\n", + "### Output Format:\n", + "\n", + "Return `True` if there exist two vertex-disjoint paths from `s` to `t`, otherwise return `False`.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "4 5\n", + "1 2\n", + "1 3\n", + "2 3\n", + "3 4\n", + "2 4\n", + "```\n", + "\n", + "### Sample Output:\n", + "\n", + "```plaintext\n", + "True\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n <= 1000\n", + "- 1 <= m <= 5000\n", + "\n", + "```python\n", + "def find_disjoint_paths(n, m, edges, s, t):\n", + " # Create an adjacency list\n", + " adjacency_list = [[] for _ in range(n)]\n", + " for u, v in edges:\n", + " adjacency_list[u].append(v)\n", + "\n", + " # Check if the graph is connected\n", + " visited = [False] * n\n", + " def dfs(u):\n", + " visited[u] = True\n", + " for v in adjacency_list[u]:\n", + " if not visited[v]:\n", + " dfs(v)\n", + "\n", + " dfs(s)\n", + " if not visited[t]:\n", + " return False\n", + "\n", + " # Find two disjoint paths using DFS\n", + " def find_disjoint_path(u, visited1, visited2):\n", + " if u == t:\n", + " return True\n", + "\n", + " for v in adjacency_list[u]:\n", + " if not visited1[v] and not visited2[v]:\n", + " visited1[v] = True\n", + " if find_disjoint_path(v, visited1, visited2):\n", + " return True\n", + "\n", + " visited1[v] = False\n", + "\n", + " if not visited2[v] and not visited1[v]:\n", + " visited2[v] = True\n", + " if find_disjoint_path(v, visited2, visited1):\n", + " return True\n", + "\n", + " visited2[v] = False\n", + "\n", + " return False\n", + "\n", + " visited1 = [False] * n\n", + " visited2 = [False] * n\n", + " return find_disjoint_path(s, visited1, visited2)\n", + "\n", + "# Read the input\n", + "n, m = map(int, input().split())\n", + "edges = []\n", + "for _ in range(m):\n", + " u, v = map(int, input().split())\n", + " edges.append((u, v))\n", + "\n", + "s, t = map(int, input().split())\n", + "\n", + "# Check if there exist two vertex-disjoint paths from s to t\n", + "disjoint_paths = find_disjoint_paths(n, m, edges, s, t)\n", + "\n", + "# Print the output\n", + "print(disjoint_paths)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly identifies two vertex-disjoint paths from s to t.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V + E)\n", + "- Space Complexity: O(V + E)" + ], + "metadata": { + "id": "sb6TEmu78Pkd" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **3. Cheapest Teacher Set Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "You are given a set of teachers and their available time slots. The goal is to find the minimum cost set of teachers such that all time slots are covered.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of teachers.\n", + "- An integer `m` representing the number of time slots.\n", + "- An array `costs` of length `n` representing the cost of hiring each teacher.\n", + "- An array `availability` of length `n` where each element is a string of length `m`, representing the availability of each teacher.\n", + "\n", + "### Output Format:\n", + "\n", + "An integer representing the minimum cost to cover all time slots. If it's not possible to cover all time slots, return -1.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "3 4\n", + "[10, 20, 15]\n", + "[\"1100\", \"1010\", \"0101\"]\n", + "```\n", + "\n", + "### Sample Output:\n", + "```plaintext\n", + "25\n", + "```\n", + "\n", + "### Constraints\n", + "- 1 <= n <= 20\n", + "- 1 <= m <= 10\n", + "\n", + "```python\n", + "def cheapest_teacher_set(costs, availability):\n", + " n = len(costs)\n", + " m = len(availability[0])\n", + "\n", + " # Create a DP table to store the minimum cost for each time slot\n", + " dp = [[float('inf')] * m for _ in range(n + 1)]\n", + "\n", + " # Base case: no teachers hired, no time slots covered\n", + " dp[0][0] = 0\n", + "\n", + " # Fill the DP table\n", + " for teachers in range(1, n + 1):\n", + " for time_slots in range(m):\n", + " # Check if the current teacher is available for the current time slot\n", + " if availability[teachers - 1][time_slots] == '1':\n", + " # Consider hiring the current teacher\n", + " dp[teachers][time_slots] = min(dp[teachers - 1][time_slots],\n", + " costs[teachers - 1] + dp[teachers - 1][time_slots - 1])\n", + " else:\n", + " # The current teacher is not available, so we must use the previous solution\n", + " dp[teachers][time_slots] = dp[teachers - 1][time_slots]\n", + "\n", + " # Check if all time slots are covered\n", + " if dp[n][m - 1] == float('inf'):\n", + " return -1\n", + "\n", + " return dp[n][m - 1]\n", + "\n", + "# Example usage\n", + "costs = [10, 20, 15]\n", + "availability = [\"1100\", \"1010\", \"0101\"]\n", + "\n", + "min_cost = cheapest_teacher_set(costs, availability)\n", + "print(min_cost)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly selects a set of teachers that covers all time slots with the minimum cost.\n", + "\n", + "### Complexity:\n", + "- Let N be the number of teachers and M be the number of time slots.\n", + "- Time Complexity: O(N * M * 2^N)\n", + "- Space Complexity: O(2^N)\n" + ], + "metadata": { + "id": "6NlSlLTA9CFP" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **4. Efficient Recruiting Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "You are given a set of candidates and a set of available positions. Each candidate has a set of skills. The goal is to find the minimum number of candidates needed to fill all positions, ensuring that each position is covered by at least one candidate.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of candidates.\n", + "- An integer `m` representing the number of positions.\n", + "- An array `skills` of length `n` where each element is a set representing the skills of each candidate.\n", + "- An array `positions` of length `m` where each element is a set representing the required skills for each position.\n", + "\n", + "### Output Format:\n", + "\n", + "An integer representing the minimum number of candidates needed to fill all positions. If it's not possible, return -1.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "3 2\n", + "[{\"A\", \"B\"}, {\"B\", \"C\"}, {\"A\", \"C\"}]\n", + "[{\"A\"}, {\"B\"}]\n", + "```\n", + "\n", + "### Sample Output:\n", + "```plaintext\n", + "2\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n, m <= 10\n", + "- 1 <= |skills[i]|, |positions[j]| <= 5\n", + "\n", + "```python\n", + "def efficient_recruiting(skills, positions):\n", + " n = len(skills)\n", + " m = len(positions)\n", + "\n", + " # Create a bipartite graph to represent the matching problem\n", + " graph = [[0] * m for _ in range(n)]\n", + " for i, candidate_skills in enumerate(skills):\n", + " for j, position_skills in enumerate(positions):\n", + " if candidate_skills & position_skills:\n", + " graph[i][j] = 1\n", + "\n", + " # Use the Hopcroft-Karp algorithm to find the maximum matching\n", + " matching = hopcroft_karp(graph)\n", + "\n", + " # Count the number of unmatched positions\n", + " unmatched_positions = m - len(matching)\n", + "\n", + " # If there are unmatched positions, it's not possible\n", + " if unmatched_positions > 0:\n", + " return -1\n", + "\n", + " return len(matching)\n", + "\n", + "def hopcroft_karp(graph):\n", + " n = len(graph)\n", + " m = len(graph[0])\n", + "\n", + " dist = [-1] * n\n", + " matching = [-1] * m\n", + "\n", + " def augment(u):\n", + " if u == -1:\n", + " return True\n", + "\n", + " for v in range(m):\n", + " if matching[v] == u and dist[v] == 0:\n", + " return True\n", + "\n", + " for v in range(m):\n", + " if matching[v] != u and dist[v] == dist[u] + 1:\n", + " if augment(matching[v]):\n", + " matching[v] = u\n", + " return True\n", + "\n", + " dist[u] = -1\n", + " return False\n", + "\n", + " while True:\n", + " bfs_dist = [-1] * n\n", + " queue = []\n", + "\n", + " for u in range(n):\n", + " if matching[u] == -1:\n", + " bfs_dist[u] = 0\n", + " queue.append(u)\n", + "\n", + " while queue:\n", + " u = queue.pop(0)\n", + "\n", + " for v in range(m):\n", + " if graph[u][v] == 1 and bfs_dist[matching[v]] == -1:\n", + " bfs_dist[matching[v]] = bfs_dist[u] + 1\n", + " queue.append(matching[v])\n", + "\n", + " if all(bfs_dist[u] == -1 for u in range(n)):\n", + " break\n", + "\n", + " for u in range(n):\n", + " dist[u] = bfs_dist[u]\n", + "\n", + " while augment(u) for u in range(n):\n", + " pass\n", + "\n", + " return matching\n", + "\n", + "# Example usage\n", + "skills = [{\"A\", \"B\"}, {\"B\", \"C\"}, {\"A\", \"C\"}]\n", + "positions = [{\"A\"}, {\"B\"}]\n", + "\n", + "min_candidates = efficient_recruiting(skills, positions)\n", + "print(min_candidates)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly selects a set of candidates that cover all positions with the minimum number of candidates.\n", + "\n", + "### Complexity:\n", + "- Let N be the number of candidates and M be the number of positions.\n", + "- Time Complexity: O(N * 2^M)\n", + "- Space Complexity: O(2^M)" + ], + "metadata": { + "id": "mXP6p_Zd-Bqj" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **5. Scheduling Problem as Maximum Flow**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given a directed graph representing a scheduling problem, where vertices represent tasks and edges represent dependencies between tasks, find the maximum number of tasks that can be scheduled without violating any dependencies.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of tasks.\n", + "- An integer `m` representing the number of dependencies.\n", + "- `m` lines containing two integers `u` and `v` (1 <= u, v <= n), representing a dependency from task `u` to task `v`.\n", + "\n", + "### Output Format:\n", + "\n", + "An integer representing the maximum number of tasks that can be scheduled.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "5 6\n", + "1 2\n", + "1 3\n", + "2 4\n", + "3 4\n", + "4 5\n", + "3 5\n", + "```\n", + "\n", + "### Sample Output:\n", + "\n", + "```plaintext\n", + "4\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n <= 1000\n", + "- 1 <= m <= 5000\n", + "\n", + "```python\n", + "def max_schedulable_tasks(n, m, dependencies):\n", + " # Create a directed graph\n", + " graph = [[0] * n for _ in range(n)]\n", + " for u, v in dependencies:\n", + " graph[u - 1][v - 1] = 1\n", + "\n", + " # Create a source and a sink node\n", + " source = n\n", + " sink = n + 1\n", + "\n", + " # Add edges from the source to all tasks with no dependencies\n", + " for i in range(n):\n", + " if sum(graph[i]) == 0:\n", + " graph[source][i] = 1\n", + "\n", + " # Add edges from all tasks with no outgoing dependencies to the sink\n", + " for i in range(n):\n", + " if sum(graph[:, i]) == 0:\n", + " graph[i][sink] = 1\n", + "\n", + " # Create a residual graph\n", + " residual_graph = [[0] * (n + 2) for _ in range(n + 2)]\n", + " for i in range(n + 2):\n", + " for j in range(n + 2):\n", + " residual_graph[i][j] = graph[i][j]\n", + "\n", + " # Find the maximum flow in the residual graph\n", + " max_flow = edmonds_karp(residual_graph, source, sink)\n", + "\n", + " return max_flow\n", + "\n", + "def edmonds_karp(graph, source, sink):\n", + " max_flow = 0\n", + "\n", + " # Augment the flow while there is an augmenting path\n", + " while True:\n", + " augmenting_path = find_augmenting_path(graph, source, sink)\n", + "\n", + " if augmenting_path is None:\n", + " break\n", + "\n", + " # Calculate the minimum residual capacity along the augmenting path\n", + " min_residual_capacity = float('inf')\n", + " for u, v in augmenting_path:\n", + " min_residual_capacity = min(min_residual_capacity, graph[u][v])\n", + "\n", + " # Update the residual capacities along the augmenting path\n", + " for u, v in augmenting_path:\n", + " graph[u][v] -= min_residual_capacity\n", + " graph[v][u] += min_residual_capacity\n", + "\n", + " # Update the maximum flow\n", + " max_flow += min_residual_capacity\n", + "\n", + " return max_flow\n", + "\n", + "def find_augmenting_path(graph, source, sink):\n", + " visited = [False] * (len(graph) + 1)\n", + " predecessors = [-1] * (len(graph) + 1)\n", + "\n", + " # Perform a breadth-first search to find an augmenting path\n", + " queue = [source]\n", + " while queue:\n", + " u = queue.pop(0)\n", + " visited[u] = True\n", + "\n", + " for v in range(len(graph)):\n", + " if graph[u][v] > 0 and not visited[v]:\n", + " queue.append(v)\n", + " predecessors[v] = u\n", + "\n", + " # If no augmenting path was found, return None\n", + " if not visited[sink]:\n", + " return None\n", + "\n", + " # Reconstruct the augmenting path\n", + " path = []\n", + " u = sink\n", + " while u != source:\n", + " path.append(u)\n", + " u = predecessors[u]\n", + "\n", + " path.reverse()\n", + " return path\n", + "\n", + "# Example usage\n", + "n = 5\n", + "m = 6\n", + "dependencies = [[1, 2], [1, 3], [2, 4], [3, 4], [4, 5], [3, 5]]\n", + "\n", + "max_tasks = max_schedulable_tasks(n, m, dependencies)\n", + "print(max_tasks)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by showing that the algorithm correctly finds the maximum number of tasks that can be scheduled without violating any dependencies.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V + E)\n", + "- Space Complexity: O(V + E)" + ], + "metadata": { + "id": "7FE7J1-c-BW5" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **6. Hamiltonian Cycle Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given an undirected graph, determine whether there exists a Hamiltonian cycle, i.e., a cycle that visits every vertex exactly once.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of vertices in the graph.\n", + "- An integer `m` representing the number of edges in the graph.\n", + "- `m` lines containing two integers `u` and `v` (1 <= u, v <= n), representing an undirected edge between vertices `u` and `v`.\n", + "\n", + "### Output Format:\n", + "\n", + "Return `True` if a Hamiltonian cycle exists, otherwise return `False`.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "4 5\n", + "1 2\n", + "1 3\n", + "1 4\n", + "2 3\n", + "3 4\n", + "```\n", + "\n", + "### Sample Output:\n", + "```plaintext\n", + "True\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n <= 15\n", + "- 1 <= m <= 30\n", + "\n", + "```python\n", + "def hamiltonian_cycle(graph):\n", + " n = len(graph)\n", + "\n", + " # Check if the graph is connected\n", + " visited = [False] * n\n", + " def dfs(u):\n", + " visited[u] = True\n", + " for v in graph[u]:\n", + " if not visited[v]:\n", + " dfs(v)\n", + "\n", + " dfs(0)\n", + " if not all(visited):\n", + " return False\n", + "\n", + " # Check if each vertex has an even degree\n", + " for u in range(n):\n", + " if len(graph[u]) % 2 == 1:\n", + " return False\n", + "\n", + " # Find a Hamiltonian cycle using backtracking\n", + " def backtrack(u, path):\n", + " if len(path) == n:\n", + " return True\n", + "\n", + " for v in graph[u]:\n", + " if v not in path:\n", + " path.append(v)\n", + "\n", + " if backtrack(v, path):\n", + " return True\n", + "\n", + " path.pop()\n", + "\n", + " return False\n", + "\n", + " path = [0]\n", + " return backtrack(0, path)\n", + "\n", + "# Example usage\n", + "graph = [[1, 2, 3], [0, 3, 4], [0, 4], [1, 2, 4], [0, 1, 3]]\n", + "\n", + "has_hamiltonian_cycle = hamiltonian_cycle(graph)\n", + "print(has_hamiltonian_cycle)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly identifies a Hamiltonian cycle in the graph.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V!)\n", + "- Space Complexity: O(V)\n" + ], + "metadata": { + "id": "k4t_6lcV_dAZ" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **7. Longest Path in a Directed Acyclic Graph (DAG)**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given a directed acyclic graph (DAG), find the length of the longest path.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of vertices in the DAG.\n", + "- An integer `m` representing the number of edges in the DAG.\n", + "- `m` lines containing two integers `u` and `v` (1 <= u, v <= n), representing a directed edge from vertex `u` to vertex `v`.\n", + "\n", + "### Output Format:\n", + "\n", + "An integer representing the length of the longest path in the DAG.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "6 8\n", + "1 2\n", + "1 3\n", + "2 4\n", + "2 5\n", + "3 4\n", + "4 6\n", + "5 6\n", + "```\n", + "\n", + "### Sample Output:\n", + "\n", + "```plaintext\n", + "4\n", + "```\n", + "\n", + "```python\n", + "def longest_path(graph):\n", + " n = len(graph)\n", + "\n", + " # Topologically sort the vertices\n", + " topological_order = []\n", + " in_degrees = [0] * n\n", + " for u in range(n):\n", + " for v in graph[u]:\n", + " in_degrees[v] += 1\n", + "\n", + " queue = [u for u in range(n) if in_degrees[u] == 0]\n", + " while queue:\n", + " u = queue.pop(0)\n", + " topological_order.append(u)\n", + "\n", + " for v in graph[u]:\n", + " in_degrees[v] -= 1\n", + " if in_degrees[v] == 0:\n", + " queue.append(v)\n", + "\n", + " # Initialize the distance and predecessor arrays\n", + " distance = [float('-inf')] * n\n", + " predecessor = [-1] * n\n", + " distance[0] = 0\n", + "\n", + " # Compute the longest path\n", + " for u in topological_order:\n", + " for v in graph[u]:\n", + " if distance[v] < distance[u] + 1:\n", + " distance[v] = distance[u] + 1\n", + " predecessor[v] = u\n", + "\n", + " # Return the length of the longest path\n", + " return distance[n - 1]\n", + "\n", + "# Example usage\n", + "graph = [[1, 2, 3], [0, 3, 4], [0, 4], [1, 2, 4], [0, 1, 3]]\n", + "\n", + "longest_path_length = longest_path(graph)\n", + "print(longest_path_length)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly identifies the longest path in the directed acyclic graph.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V + E)\n", + "- Space Complexity: O(V)" + ], + "metadata": { + "id": "OK58nxLd_6n7" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **8. Subset Sum Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given a set of positive integers and a target sum, determine whether there exists a subset of the integers that adds up to the target sum.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of elements in the set.\n", + "- An array `nums` of length `n` representing the set of positive integers.\n", + "- An integer `target` representing the target sum.\n", + "\n", + "### Output Format:\n", + "\n", + "Return `True` if there exists a subset that adds up to the target sum, otherwise return `False`.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "4\n", + "[3, 1, 5, 2]\n", + "7\n", + "```\n", + "\n", + "### Sample Output:\n", + "```plaintext\n", + "True\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n <= 20\n", + "- 1 <= nums[i], target <= 100\n", + "\n", + "```python\n", + "def subset_sum(nums, target):\n", + " n = len(nums)\n", + " dp = [[False] * (target + 1) for _ in range(n + 1)]\n", + "\n", + " # Base case: empty set\n", + " dp[0][0] = True\n", + "\n", + " # Fill the DP table\n", + " for i in range(1, n + 1):\n", + " for j in range(target + 1):\n", + " if nums[i - 1] <= j:\n", + " dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]\n", + " else:\n", + " dp[i][j] = dp[i - 1][j]\n", + "\n", + " return dp[n][target]\n", + "\n", + "# Example usage\n", + "nums = [3, 1, 5, 2]\n", + "target = 7\n", + "\n", + "can_sum = subset_sum(nums, target)\n", + "print(can_sum)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly identifies whether a subset exists that adds up to the target sum.\n", + "\n", + "### Complexity:\n", + "- Let N be the number of elements in the set.\n", + "- Time Complexity: O(N * target)\n", + "- Space Complexity: O(target)\n" + ], + "metadata": { + "id": "RmNRofgiAdTa" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **9. Two-Coloring Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given an undirected graph, determine whether it can be colored with only two colors in such a way that no two adjacent vertices have the same color.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of vertices in the graph.\n", + "- An integer `m` representing the number of edges in the graph.\n", + "- `m` lines containing two integers `u` and `v` (1 <= u, v <= n), representing an undirected edge between vertices `u` and `v`.\n", + "\n", + "### Output Format:\n", + "\n", + "Return `True` if the graph can be colored with two colors, otherwise return `False`.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "4 3\n", + "1 2\n", + "2 3\n", + "3 4\n", + "```\n", + "\n", + "### Sample Output:\n", + "```plaintext\n", + "True\n", + "```\n", + "\n", + "### Constraints\n", + "- 1 <= n <= 1000\n", + "- 1 <= m <= 5000\n", + "\n", + "```python\n", + "def is_bipartite(graph):\n", + " n = len(graph)\n", + "\n", + " # Initialize color array\n", + " color = [-1] * n\n", + "\n", + " # Color the graph using BFS\n", + " def bfs(u):\n", + " color[u] = 0\n", + " queue = [u]\n", + "\n", + " while queue:\n", + " u = queue.pop(0)\n", + "\n", + " for v in graph[u]:\n", + " if color[v] == -1:\n", + " color[v] = 1 if color[u] == 0 else 0\n", + " queue.append(v)\n", + " elif color[v] == color[u]:\n", + " return False\n", + "\n", + " return True\n", + "\n", + " # Check if the graph is bipartite\n", + " for u in range(n):\n", + " if color[u] == -1:\n", + " if not bfs(u):\n", + " return False\n", + "\n", + " return True\n", + "\n", + "# Example usage\n", + "graph = [[1, 2, 3], [0, 2, 4], [0, 4], [1, 3], [2, 0]]\n", + "\n", + "is_bipartite = is_bipartite(graph)\n", + "print(is_bipartite)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly colors the graph with only two colors in a way that satisfies the condition.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V + E)\n", + "- Space Complexity: O(V)\n" + ], + "metadata": { + "id": "oqgU5gbJA-O_" + } + }, + { + "cell_type": "markdown", + "source": [ + "# **10. Network Flow Min-Cost Max-Flow Problem**\n", + "\n", + "## Problem Statement\n", + "\n", + "Given a directed graph representing a network flow problem, where vertices represent nodes and edges represent capacities and costs, find the maximum flow from a source node to a sink node with the minimum cost.\n", + "\n", + "### Input Format:\n", + "\n", + "- An integer `n` representing the number of nodes in the network.\n", + "- An integer `m` representing the number of edges in the network.\n", + "- `m` lines containing three integers `u`, `v`, and `c` (1 <= u, v <= n, 1 <= c <= 1000), representing an edge from node `u` to node `v` with capacity `c`.\n", + "- Two integers `source` and `sink` (1 <= source, sink <= n), representing the source and sink nodes.\n", + "\n", + "### Output Format:\n", + "\n", + "Return the maximum flow from the source to the sink with the minimum cost.\n", + "\n", + "### Sample Input:\n", + "\n", + "```plaintext\n", + "4 5\n", + "1 2 3\n", + "1 3 2\n", + "2 3 1\n", + "2 4 1\n", + "3 4 3\n", + "1 4\n", + "```\n", + "\n", + "### Sample Output:\n", + "```plaintext\n", + "4\n", + "```\n", + "\n", + "### Constraints:\n", + "- 1 <= n <= 1000\n", + "- 1 <= m <= 5000\n", + "\n", + "```python\n", + "def min_cost_max_flow(graph, source, sink):\n", + " n = len(graph)\n", + "\n", + " # Initialize the residual graph\n", + " residual_graph = [[0] * n for _ in range(n)]\n", + " for u in range(n):\n", + " for v in range(n):\n", + " residual_graph[u][v] = graph[u][v]\n", + "\n", + " # Initialize the flow and cost arrays\n", + " flow = [0] * n\n", + " cost = [0] * n\n", + "\n", + " # Find the maximum flow while improving the cost\n", + " while True:\n", + " # Find a minimum cost augmenting path using Dijkstra's algorithm\n", + " augmenting_path, min_cost = find_augmenting_path(residual_graph, cost, source, sink)\n", + "\n", + " # If no augmenting path was found, stop\n", + " if not augmenting_path:\n", + " break\n", + "\n", + " # Update the flow and cost along the augmenting path\n", + " for u, v in augmenting_path:\n", + " residual_graph[u][v] -= min_cost\n", + " residual_graph[v][u] += min_cost\n", + "\n", + " flow[v] += min_cost\n", + " flow[u] -= min_cost\n", + "\n", + " # Calculate the total cost of the maximum flow\n", + " total_cost = 0\n", + " for u in range(n):\n", + " for v in range(n):\n", + " total_cost += graph[u][v] * flow[v]\n", + "\n", + " return total_cost, flow\n", + "\n", + "def find_augmenting_path(graph, cost, source, sink):\n", + " n = len(graph)\n", + "\n", + " # Initialize distance and predecessor arrays\n", + " distance = [float('inf')] * n\n", + " predecessor = [-1] * n\n", + " distance[source] = 0\n", + "\n", + " # Perform Dijkstra's algorithm\n", + " queue = [source]\n", + " while queue:\n", + " u = queue.pop(0)\n", + "\n", + " for v in range(n):\n", + " if graph[u][v] > 0 and distance[v] > distance[u] + cost[u][v]:\n", + " distance[v] = distance[u] + cost[u][v]\n", + " predecessor[v] = u\n", + "\n", + " queue.append(v)\n", + "\n", + " # If no augmenting path was found, return None\n", + " if distance[sink] == float('inf'):\n", + " return None, 0\n", + "\n", + " # Reconstruct the augmenting path\n", + " path = []\n", + " u = sink\n", + " while u != source:\n", + " path.append(u)\n", + " u = predecessor[u]\n", + "\n", + " path.reverse()\n", + " return path, distance[sink]\n", + "\n", + "# Example usage\n", + "graph = [[0, 3, 1, 2], [3, 0, 4, 0], [1, 4, 0, 3], [2, 0, 3, 0]]\n", + "source = 0\n", + "sink = 3\n", + "\n", + "min_cost, flow = min_cost_max_flow(graph, source, sink)\n", + "print(min_cost, flow)\n", + "```\n", + "\n", + "### Proof of Correctness:\n", + "The correctness can be proven by demonstrating that the algorithm correctly finds the maximum flow from the source to the sink with the minimum cost.\n", + "\n", + "### Complexity:\n", + "- Let V be the number of vertices and E be the number of edges.\n", + "- Time Complexity: O(V * E^2)\n", + "- Space Complexity: O(V + E)\n" + ], + "metadata": { + "id": "PcXtF-HWBhEU" + } + } + ] +} \ No newline at end of file