Your gateway to knowledge, innovation, and well-being.
The "Design and Analysis of Algorithms" course emphasizes understanding, designing, and evaluating algorithms to solve computational problems effectively. It delves into topics such as recursion, graph algorithms, sorting and searching, and advanced data structures like trees and heaps. Students gain skills in analyzing algorithm efficiency in terms of time and space complexity, applying these methods to real-world problems, and building a strong foundation for tackling advanced computing challenges.
data-driven urban planning system for smart cities with minimal vehicle usage. It will use efficient data structures and algorithms to optimize transportation, resource management, and governance while promoting environmental sustainability and enhancing tourism opportunities. The system will focus on leveraging real-time data and predictive analytics to ensure efficient, eco-friendly city operations.
KWIN City is built with tomorrow in mind, designed to bring a better future today. At the heart of the city lies its people, with every element crafted to empower the community. Here, every doorway opens to new learning opportunities, every pillar supports cutting-edge innovations, and every path leads toward holistic well-being.
KWIN City is not just a place, but a community built on values that matter. With a strong foundation in collaboration, conservation, and coexistence, the city reflects a commitment to the environment and social harmony.
Our city is rich in green promises, creating a sustainable environment where nature and people coexist seamlessly. KWIN isn’t just designed for the future; it is designed by and for the people, making it a destination like no other. The city inspires a new way of life, focused on innovation, sustainability, and inclusivity.
Well-Connected, Convenient, and Accessible
KWIN City enjoys a prime location strategically positioned between Doddaballapur and Dobbaspet, ensuring easy access to key regions. Just 45 minutes from Kempegowda International Airport, the city is well-connected by major transportation routes.
With this connectivity, KWIN City is perfectly situated for regional and global growth, making it an ideal place for business, innovation, and community.
Explore the layout of KWIN City.
SLNO | Business Case | Description | Owner | Tools |
---|---|---|---|---|
1 | Optimal Tourist Routes | Route optimization for tourists. | Varun S T | Graphs |
2 | Menu Item Sorting | Sort menus by price/popularity. | Vishal U H | Quick Sort |
3 | Traffic Monitoring | Analyze intersection traffic density. | Saif Ali | BFS, DFS |
4 | Waste Management | Efficient waste collection routes. | Suleman A | Dijkstra's |
5 | Water Usage | Residential water analysis. | Vishal and Varun | Time Series |
6 | Internet Connectivity | Optimize city-wide Wi-Fi. | Saif and Suleman | Minimum Spanning Tree |
This initiative focuses on developing a system that leverages real-time data like traffic conditions, crowd density, and user preferences to recommend ideal travel routes for tourists. Using graph-based algorithms, the system ensures that visitors have a seamless and enjoyable experience while reducing the pressure on popular tourist locations. By optimizing routes, it also supports sustainable urban tourism and enhances the visibility of lesser-known attractions.
Sorting menu items efficiently is crucial for e-commerce and digital platforms in food and retail industries. This system uses quick sort and array structures to organize items based on price or popularity, offering a streamlined and intuitive interface for users. By dynamically updating sorting criteria based on real-time data, the solution ensures an enhanced customer experience and supports better decision-making.
Managing traffic density at critical intersections is essential for promoting vehicle-free smart cities. This case employs graph traversal algorithms like BFS and DFS to analyze traffic patterns, identify bottlenecks, and propose alternate routes. The solution supports proactive traffic management, ensuring smoother flow and reducing environmental impact.
Efficient waste collection is vital for urban hygiene and sustainability. This system applies Dijkstra's Algorithm to determine optimal routes for waste collection vehicles, minimizing fuel consumption and travel time. It not only reduces operational costs but also contributes to lower emissions, supporting green city initiatives.
Water conservation is a critical concern in urban planning. This system tracks and analyzes residential water usage patterns using time-series analysis. By identifying trends and irregularities, it provides actionable insights for policymakers and promotes responsible water consumption among residents.
Reliable internet connectivity is foundational to smart cities. This initiative maps and optimizes the placement of Wi-Fi hotspots using minimum spanning tree algorithms. By ensuring high-speed connectivity and minimizing redundant infrastructure, the system enhances digital inclusivity and supports the city's technological growth.
Many concepts, such as time complexity, graph algorithms, or dynamic programming, are highly abstract. They can be difficult to visualize without concrete examples or real-world applications, making it harder to grasp the theory. Abstract concepts like Big O notation require not only mathematical reasoning but also an understanding of how these concepts affect the overall performance and behavior of programs.
Understanding these concepts often requires a solid foundation in mathematics, particularly discrete mathematics, linear algebra, and set theory. Students without this background may struggle with logic, proofs, or formal algorithm analysis. Building this mathematical proficiency can be a barrier for many.
Many algorithmic concepts are deeply interrelated, and grasping one concept often requires a sound understanding of others. For instance, understanding sorting algorithms is vital for comprehending how certain graph traversal algorithms (like Dijkstra’s or Prim’s algorithm) work efficiently. This interconnectedness can create a compounding effect, where mastering one concept becomes necessary before others can be fully understood.
Translating abstract theoretical concepts into practical, executable code can be a daunting challenge. Concepts like recursion, backtracking, or memoization are essential to implementing many algorithms. Students must also understand the subtleties of memory management (e.g., stack vs heap) and optimization techniques (e.g., loop unrolling, caching), which can be difficult to grasp without experience in low-level programming.
In theoretical settings, algorithms often assume ideal conditions (e.g., perfectly sorted data, unlimited processing power). However, real-world applications often deal with imperfect data, unreliable network conditions, and hardware limitations. For example, sorting algorithms like quicksort assume that the data is distributed evenly, which may not always be the case.
Real-world applications often require balancing trade-offs between competing factors, such as time complexity, memory usage, and the responsiveness of the system. For instance, a real-time system might prioritize speed over memory usage, whereas a mobile application might prioritize battery life.
Algorithms often perform unpredictably in the real world due to the inherent randomness of data and user behavior. For example, a search algorithm might be highly efficient on a static dataset but may fail to perform well when the data is constantly changing or when faced with inconsistent input sizes.
Integrating algorithms into existing software often presents practical challenges. This can involve adjusting the algorithm to work within the constraints of legacy systems, working with different programming languages, or optimizing an algorithm to be compatible with a specific hardware environment.
The process of breaking down a complex problem into smaller, more manageable subproblems is essential in designing efficient algorithms. By applying the divide and conquer strategy, for example, problems can be divided into simpler parts that are easier to solve independently. Once the smaller problems are solved, their solutions are combined to solve the overall problem. This technique is commonly used in algorithms like merge sort or quick sort, and also in solving matrix multiplication problems through the Strassen algorithm.
This approach not only makes problems more tractable but also often results in more efficient solutions. For instance, dynamic programming (DP) is another decomposition technique where a problem is broken into overlapping subproblems, and intermediate results are stored (memoization) to avoid redundant calculations, improving efficiency.
It is crucial to analyze and compare the time and space complexity of different algorithms to select the most efficient one for a given problem. The Big O notation is a valuable tool for representing the worst-case time complexity of an algorithm, helping developers choose the best algorithm based on the input size. For example, while a brute-force solution might be simple to implement, its higher time complexity (e.g., O(n^2) in the case of bubble sort) can be less efficient than more sophisticated algorithms like quicksort (O(n log n)) for larger data sets.
In addition to Big O, other complexities, such as Big Ω (omega) and Big Θ (theta), are important in capturing the best-case and average-case performance. By considering these different cases, developers can more precisely evaluate how the algorithm will perform in various conditions.
Choosing the appropriate data structure is critical for ensuring efficient performance. For example, if quick access to elements by index is required, an array or hash table may be ideal, but if ordered data needs to be maintained or frequent insertions/deletions are required, binary search trees (BSTs) or heaps may offer better performance. For instance, in graph algorithms, the decision to use an adjacency list or adjacency matrix can drastically affect performance based on the graph’s sparsity.
Understanding the strengths and weaknesses of data structures like stacks, queues, linked lists, trees, and graphs enables developers to make informed decisions and optimize algorithm performance based on the specific problem at hand.
Optimizing code is often an iterative process that involves identifying performance bottlenecks through profiling tools. By analyzing code performance with tools like gprof or Valgrind in C/C++ or cProfile in Python, developers can pinpoint areas of inefficiency—such as unnecessary memory allocations, repeated computations, or excessive function calls— and focus on optimizing those specific sections.
After identifying critical bottlenecks, developers can experiment with different techniques, such as loop unrolling, memoization, or parallelization, to reduce time complexity. This is particularly useful in environments with stringent resource constraints, such as mobile devices or embedded systems, where optimization is often necessary for achieving real-time performance.
As students dive deeper into algorithmic concepts, they encounter a wealth of new terminology and notations. The language used in algorithms—like "recursion," "graph traversal," or "dynamic programming"—can be a barrier itself. Understanding how to interpret symbols such as Big O notation (for time/space complexity), tree traversal orders (preorder, inorder, postorder), or graph-based notations (adjacency list/matrix) is a significant hurdle.
The shift from imperative thinking to recursive or algorithmic thinking can be difficult. For example, understanding recursion involves thinking about problems in terms of their subproblems, which is very different from the traditional linear flow of control seen in iterative algorithms.
Debugging is often more challenging with complex algorithms due to their intricate logic. For example, tree and graph traversal algorithms have many recursive or iterative steps, making it harder to track the program's state during execution. It’s easy to introduce subtle bugs, such as infinite loops, off-by-one errors, or logical errors in base cases.
Many algorithmic problems, especially in graph theory, dynamic programming, or tree structures, are difficult to visualize without appropriate diagrams. Visualizing data structures like AVL trees, heaps, or linked lists and understanding how they are manipulated during algorithm execution can be a significant challenge. Having access to interactive tools that can help students visualize algorithm execution can greatly enhance understanding.
In real-world scenarios, data is rarely perfect. Missing values, noise, and outliers can affect the performance of algorithms. For instance, an algorithm designed for clean data may break down or perform poorly when faced with inconsistent or missing input.
Real-world systems evolve over time, and algorithms must be adaptable to handle changes in the environment. For example, an algorithm designed to route traffic in a city must account for changes in traffic patterns, construction, or accidents.
Algorithms can have significant social, ethical, and legal implications. Issues such as algorithmic bias, fairness, and privacy concerns must be considered when applying algorithms to real-world problems. For instance, biased data can lead to discriminatory outcomes in predictive policing or hiring algorithms.
Algorithms need to be tailored to specific real-world domains. For example, understanding the structure of data in a financial application is different from that in a healthcare or social media application. Deep knowledge of the domain ensures that the algorithm can be fine-tuned to achieve better performance and results.
Greedy algorithms are often used for optimization problems where the goal is to make a series of locally optimal choices with the hope of finding a globally optimal solution. For example, in the coin change problem, a greedy algorithm might choose the largest denomination first, but this does not always result in the most efficient solution in all cases. Therefore, although greedy algorithms are quick and simple to implement, their correctness depends on the problem's structure.
In cases where greedy algorithms do not work optimally, it’s essential to use other techniques like dynamic programming or backtracking to guarantee an optimal solution. The key is to analyze the problem carefully to see if a greedy approach is applicable or if a more complex solution is required.
The divide and conquer technique divides the problem into subproblems, solves them recursively, and then combines their results. This approach is widely used in algorithms like merge sort, quick sort, and binary search, which leverage the simplicity and efficiency of breaking a large problem down. For instance, merge sort divides the list in half, sorts each half, and then merges them together efficiently, ensuring an optimal time complexity of O(n log n).
The divide and conquer approach can be combined with other techniques, such as dynamic programming or greedy methods, depending on the problem's nature. When solving a problem with this technique, it is crucial to define a clear recursive structure and understand how to efficiently combine the results.
Dynamic programming is a powerful technique that optimizes recursive algorithms by storing the results of subproblems in a table (memoization), avoiding redundant work. This is particularly helpful for problems that exhibit overlapping subproblems, such as the Knapsack problem or Fibonacci sequence. For example, the naive recursive Fibonacci algorithm has an exponential time complexity, but dynamic programming reduces it to linear time by storing intermediate results.
Dynamic programming can be categorized into two types: top-down (memoization), where the problem is solved recursively and results are cached, and bottom-up (tabulation), where an iterative approach is used to build up solutions from smaller subproblems. Both methods help improve time efficiency, but tabulation tends to use less memory.
Approximation algorithms are essential for solving NP-hard problems, where finding an exact solution is computationally expensive. These algorithms are designed to find near-optimal solutions within a guaranteed bound of the optimal answer. For example, the Traveling Salesman Problem (TSP) can be tackled using approximation algorithms that find a solution close to the optimal path, even if it’s not guaranteed to be the best one. This trade-off is acceptable in many real-world applications where an exact solution is unnecessary, and time constraints make finding the optimal solution impractical.
These concepts often require an extensive time investment to master. Not only do students need to read and study textbooks, but they must also implement and test these algorithms, which can be time-consuming. Working through exercises and solving programming problems on platforms like LeetCode, HackerRank, or Codeforces is essential for reinforcing understanding.
Given the complexity of these topics, students may feel overwhelmed or discouraged when progress seems slow. Motivation is key to pushing through difficult concepts, and maintaining persistence during setbacks, such as failing to understand a tricky algorithm, is necessary for success.
Students who are new to programming might find algorithmic thinking particularly challenging. Understanding the theoretical aspects of algorithms is one thing, but translating that into a working implementation requires practical experience in coding, debugging, and optimizing code, which may not be easily accessible to beginners.
The way instructors present these concepts can significantly impact learning. A poorly explained concept or a lack of real-world examples can make abstract concepts even harder to grasp. Conversely, engaging instructors who use diverse teaching methods and provide hands-on learning opportunities can make a significant difference in student comprehension.
Real-world problems often involve multiple variables and constraints. Algorithms need to be able to handle a range of situations and integrate with other systems to produce viable solutions. For example, an e-commerce recommendation system involves not just user preferences but also product inventory, shipping constraints, and seasonal trends.
Users may behave in unexpected ways, which can influence how algorithms should be designed. For example, a recommendation algorithm for an online video platform needs to handle diverse user tastes and account for behaviors like skipping content, watching out of sequence, or ignoring recommendations entirely.
Real-world systems are usually constrained by limited resources such as memory, processing power, and network bandwidth. For example, an embedded system may not be able to support complex algorithms, so more efficient and lightweight alternatives are necessary.
Systems that involve large numbers of components, such as social networks, often exhibit emergent behavior that is difficult to predict. For example, in a large-scale social media platform, user interactions can generate unexpected viral phenomena, and algorithmic decisions must be robust enough to handle such complexities.
In real-world software engineering, especially in the context of algorithms, it's common to develop an initial prototype that captures the core idea. The prototype serves as a proof of concept, allowing developers to test the algorithm in real scenarios. Feedback from testing helps refine the solution, optimize performance, and fix any bugs. For instance, building a recommendation system might begin with a simple algorithm like content-based filtering before transitioning to more complex collaborative filtering methods.
Iterative development helps in adapting to changing requirements or discovering edge cases that weren’t initially considered. It’s also essential to gather user feedback during the iteration process to ensure the algorithm meets practical needs and real-world constraints.
The process of experimentation involves trying multiple approaches to solve a problem and evaluating their performance. By testing various algorithms under different conditions, developers can determine which method yields the best results. This is especially valuable when applying machine learning algorithms, where different models, hyperparameters, or training techniques can be explored and compared.
Experimentation is also vital in optimizing algorithms—by testing with real-world data sets, developers can measure algorithm efficiency and adjust accordingly. For example, comparing heap sort with merge sort on a specific data set may reveal the better performer, depending on whether the data is partially sorted or entirely random.
Utilizing pre-built libraries and frameworks can significantly improve the efficiency and speed of software development. Instead of re-implementing common algorithms, developers can use highly optimized libraries like NumPy (for numerical computations), Scikit-learn (for machine learning), or Boost (for graph algorithms) that provide optimized implementations and handle edge cases effectively.
Leveraging libraries saves valuable development time and ensures that the algorithms are already optimized for performance. However, it’s important to understand the trade-offs between general-purpose libraries and custom implementations, especially when working under specific constraints.
Staying updated with new research and developments in algorithms and data structures is essential. The field of algorithms is rapidly evolving, with new techniques, optimizations, and best practices emerging regularly. Attending conferences, reading academic papers, and following industry leaders can help developers stay at the cutting edge. This continuous learning can lead to more efficient solutions and a deeper understanding of emerging trends like quantum computing or graph neural networks, which may impact future algorithm designs.
The complexity of algorithms can sometimes make students feel like they are not capable of mastering them. This can lead to self-doubt and the fear of not measuring up to peers or instructors. Overcoming these feelings is crucial for staying motivated and continuing to learn
The learning process often involves processing multiple layers of complexity. This includes learning new terminology, understanding the nuances of various data structures, and applying them to solve real-world problems. Cognitive overload can make it difficult for students to retain and apply knowledge effectively.
Without exposure to real-world applications, it can be difficult for students to see the value of algorithmic knowledge. For example, knowing how to efficiently sort data or find the shortest path in a graph becomes much more relevant when working on real projects like web development or data analytics.
While there is an abundance of resources online, not all resources are of high quality or accessible. Books, online courses, and mentoring from experienced professionals can significantly improve learning, but not everyone has access to such resources, especially in certain geographic locations or financial circumstances.
Many students struggle to apply what they learn in a classroom to actual problems in the field. Theoretical knowledge might not always transfer smoothly to real-world projects, where problems are messy, the environment is dynamic, and constraints are real.
The absence of real-world data is a common issue. Many students work with academic datasets, which, while helpful for understanding basic concepts, may not accurately reflect the complexity and messiness of data in actual applications.
Some academic programs emphasize theoretical learning and may not give students enough opportunities to apply their knowledge to real-world problems. Without hands-on practice, students may struggle when they encounter practical challenges in applying algorithms.
Applying algorithms in a team environment requires excellent communication skills. Collaborating with domain experts or other software engineers who are focused on different aspects of the project can be challenging for students who are used to working independently or on theoretical problems.
Before diving into the design of any algorithm, it’s crucial to have a clear understanding of the problem’s constraints—whether that’s time, memory, processing power, or network bandwidth. These constraints will shape decisions around which algorithms or techniques to apply. For example, in mobile app development, energy consumption is a primary constraint, so the algorithm should prioritize lower power usage, such as using O(n log n) algorithms instead of brute-force approaches.
Understanding the specific context of the problem allows for more targeted and effective algorithm design.
In designing algorithms, focusing on efficiency from the outset helps avoid costly rework later. Whether that’s minimizing time complexity to handle large inputs or optimizing memory usage to fit within hardware constraints, prioritizing efficiency helps ensure the algorithm meets performance requirements.
Efficiency can also extend to code readability and maintainability. Efficient code isn’t just about performance—it’s about finding the right balance between clarity and optimization.
Thorough testing is essential to ensure that the designed solution works under all expected conditions. This includes unit tests, integration tests, and performance tests to ensure the algorithm behaves correctly across a range of input data. Load testing can be particularly useful to check whether the algorithm holds up under heavy traffic or large data sets, as might be encountered in web applications or distributed systems.
Successful algorithm design is often a collaborative effort. Developers must work closely with domain experts, project managers, and other stakeholders to understand requirements and constraints. Effective communication ensures that the chosen solution meets both technical and business needs, while collaboration allows for the sharing of knowledge, ideas, and strategies that can lead to a more efficient solution.
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <map>
#include <string>
#include <numeric> // For accumulate function
#include <climits> // For INT_MAX
using namespace std;
// Function for Optimal Tourist Routes (Graphs)
void optimalTouristRoutes() {
cout << "Optimal Tourist Routes using Graphs\n";
int n;
cout << "Enter the number of nodes: ";
cin >> n;
vector<vector<int>> graph(n, vector<int>(n, 0));
cout << "Enter the adjacency matrix (distance between nodes):\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> graph[i][j];
}
}
cout << "Optimal route (example): 0 -> 1 -> 3 -> 2\n";
}
// Function for Menu Item Sorting (Quick Sort)
void menuItemSorting() {
cout << "Menu Item Sorting by Price or Popularity\n";
int n;
cout << "Enter the number of menu items: ";
cin >> n;
vector<pair<string, int>> menu;
for (int i = 0; i < n; ++i) {
string item;
int price;
cout << "Enter item name and price: ";
cin >> item >> price;
menu.emplace_back(item, price);
}
sort(menu.begin(), menu.end(), [](pair<string, int> a, pair<string, int> b) {
return a.second < b.second; // Sort by price (ascending)
});
cout << "Sorted Menu:\n";
for (auto item : menu) {
cout << item.first << " - $" << item.second << "\n";
}
}
// Function for Traffic Monitoring (BFS)
void trafficMonitoring() {
cout << "Traffic Monitoring using BFS\n";
int n;
cout << "Enter the number of nodes: ";
cin >> n;
vector<vector<int>> graph(n, vector<int>(n, 0));
cout << "Enter the adjacency matrix (1 for connected, 0 for not connected):\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> graph[i][j];
}
}
vector<bool> visited(n, false);
queue<int> q;
q.push(0);
visited[0] = true;
cout << "BFS Order: ";
while (!q.empty()) {
int node = q.front();
q.pop();
cout << node << " ";
for (int i = 0; i < n; ++i) {
if (graph[node][i] == 1 && !visited[i]) {
visited[i] = true;
q.push(i);
}
}
}
cout << "\n";
}
// Function for Waste Management (Dijkstra's)
void wasteManagement() {
cout << "Waste Management Route Optimization using Dijkstra's\n";
int n, e;
cout << "Enter the number of nodes and edges: ";
cin >> n >> e;
vector<vector<pair<int, int>>> graph(n);
cout << "Enter the edges (format: node1 node2 weight):\n";
for (int i = 0; i < e; ++i) {
int u, v, w;
cin >> u >> v >> w;
graph[u].emplace_back(v, w);
}
vector<int> dist(n, INT_MAX);
dist[0] = 0;
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
pq.push({0, 0});
while (!pq.empty()) {
auto [cost, node] = pq.top();
pq.pop();
for (auto [next, weight] : graph[node]) {
if (dist[next] > cost + weight) {
dist[next] = cost + weight;
pq.push({dist[next], next});
}
}
}
cout << "Shortest distances from Node 0: ";
for (int d : dist) cout << d << " ";
cout << "\n";
}
// Function for Water Usage Analysis (Time Series)
void waterUsageAnalysis() {
cout << "Water Usage Analysis using Time Series\n";
int n;
cout << "Enter the number of usage entries: ";
cin >> n;
vector<int> usage(n);
cout << "Enter the water usage data:\n";
for (int i = 0; i < n; ++i) {
cin >> usage[i];
}
cout << "Average Usage: "
<< accumulate(usage.begin(), usage.end(), 0) / usage.size()
<< "\n";
}
// Function for Internet Connectivity (Minimum Spanning Tree)
void internetConnectivity() {
cout << "Internet Connectivity Optimization using MST\n";
int n;
cout << "Enter the number of nodes: ";
cin >> n;
vector<vector<int>> graph(n, vector<int>(n, 0));
cout << "Enter the adjacency matrix (cost of edges):\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> graph[i][j];
}
}
vector<int> key(n, INT_MAX);
vector<bool> inMST(n, false);
key[0] = 0;
int result = 0;
for (int i = 0; i < n; ++i) {
int u = -1;
for (int v = 0; v < n; ++v) {
if (!inMST[v] && (u == -1 || key[v] < key[u])) {
u = v;
}
}
inMST[u] = true;
result += key[u];
for (int v = 0; v < n; ++v) {
if (graph[u][v] && !inMST[v] && graph[u][v] < key[v]) {
key[v] = graph[u][v];
}
}
}
cout << "Total Cost of MST: " << result << "\n";
}
// Main Function
int main() {
while (true) {
cout << "\nChoose an option:\n";
cout << "1. Optimal Tourist Routes\n";
cout << "2. Menu Item Sorting\n";
cout << "3. Traffic Monitoring\n";
cout << "4. Waste Management\n";
cout << "5. Water Usage Analysis\n";
cout << "6. Internet Connectivity Optimization\n";
cout << "7. Exit\n";
int choice;
cin >> choice;
switch (choice) {
case 1: optimalTouristRoutes(); break;
case 2: menuItemSorting(); break;
case 3: trafficMonitoring(); break;
case 4: wasteManagement(); break;
case 5: waterUsageAnalysis(); break;
case 6: internetConnectivity(); break;
case 7: return 0;
default: cout << "Invalid choice. Try again.\n";
}
}
return 0;
}