- size(): returns size of the string
- getline(cin, str): takes the whole line as input
- To take very large data as input, use strings and then write proper algos to deal with them
- Try not to add characters to string directly, instead use push_back() function to append characters to the string
#include <bits/stdc++.h>
using namespace std;
int main() {
string s1 = "Hello";
// string s2 = "World";
string s2 = "Hello";
cout << s1 + " " + s2 << endl;
if (s1 == s2) {
cout << "Equal" << endl;
}
else {
cout << "Not equal" << endl;
}
// to access elements we can use it by using the index
// each element is a character
s1[0] = 'c';
// we can re-assign individual characters of a string
cout << s1.size() << endl;
for (int i = 0; i < s1.size(); i++) {
cout << s1[i] << " ";
}
cout << endl;
// to take the whole line as input
int t;
cin >> t;
cin.ignore(); // so that we can ignore extra space
while (t--)
{
string str;
getline(cin, str);
cout << str << endl;
}
// reverse a string
string ans;
cin >> ans;
for (int i = 0; i < ans.size(); i++) {
ans.push_back(ans[i]); // don't add characters to string, not a good practice
}
return 0;
}
- Vectors are dynamic arrays with no fixed size.
- v[i] isn't accessible until there's something inside the vector.
- Pairs are containers used to store related data.
- When a pair is swapped the whole pair is swapped.
#include <bits/stdc++.h>
using namespace std;
void printVec(vector<int>& v) { // passed by reference because otherwise it will pass a copy
// and that takes O(N) time complexity
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
}
int main() {
vector<int> v(10, -1); // size = 10, all values = -1
vector<int> v1 = v; // makes a copy of v into v1, takes O(N) time complexity
vector<int>& v2 = v; // makes a reference variable of v
printVec(v); //print the vector
pair<int, int> p;
// p = make_pair(2, 3);
p = { 2,3 }; // another way to initialize pair, more handy
int a = p.first; // gives the first value
int b = p.second; // gives the second value
// suppose we needed relation between elements of two arrays
int arr[3] = { 1,2,3 };
int brr[3] = { 4,5,6 };
pair<int, int> p1[3];
p1[0] = { arr[0],brr[0] };
p1[1] = { arr[1],brr[1] };
p1[2] = { arr[2],brr[2] };
swap(p1[0], p1[2]); // can swap easily obviously.
for (int i = 0; i < 3; i++) {
cout << p1[i].first << " " << p1[i].second << endl;
}
return 0;
}
- Pairs can be nested in the vectors to form vector of pair, where each element is a pair instead of 'int' or 'char' or 'bool'.
- Vectors can be nested to form 2D dynamic arrays.
- Remember, that in case of nested vectors each
v[i]
represents a vector, so to access a 2D or 3D vector usingv[i]
there needs to be some vector(s) inside it.
#include <bits/stdc++.h>
using namespace std;
void printVec(vector<pair<int, int>>& v) {
cout << "size: " << v.size() << endl;
for (int i = 0; i < v.size(); i++) {
cout << v[i].first << " " << v[i].second << endl;
}
}
void printVec(vector<int>& v) {
cout << "size: " << v.size() << endl;
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
}
int main() {
// vector of pairs
vector<pair<int, int>> v = {
{1,2}, {2,3}, {3,4}
};
printVec(v);
// array of vectors
int N; // number of vectors inside the array
cin >> N;
vector<int> v1[N];
for (int i = 0; i < N; i++) {
int n; // size of each vector
cin >> n;
for (int j = 0; j < n; j++) {
int x; // value inside each vector
cin >> x;
v1[i].push_back(x); // pushing x in the ith vector
}
}
for (int i = 0; i < N; i++) {
printVec(v1[i]);
}
vector<vector<int>> v2; // Vector of vectors : v2[i] -> vector
int K;
cin >> K;
// now v2[i] cannot be accessed until there's something inside it.
// so either create a temp vector and push everything in it and then
// push it in the v2 vector or v2.push_back(vector<int> ());
// Both will work
for (int i = 0; i < K; i++) {
int n;
cin >> n;
vector<int> temp;
for (int j = 0; j < n; j++) {
int x;
cin >> x;
temp.push_back(x);
}
v2.push_back(temp);
}
// print v2
cout << "v2: " << endl;
for (int i = 0; i < v2.size(); i++) {
printVec(v2[i]);
}
return 0;
}
- Iterators point to the elements of a container.
- Syntax :
container_type :: iterator it
, 'it' is the name of iterator, it could be named anything obviously. begin()
points to the start position whileend()
points next to the last element of the container, NOT the last element but next to the last.- In case of iterators there's a difference between
it++
andit+1
- For vectors it's the same because vector has contiguous memory allocation, but in case of maps and sets, the memory allocation couldn't be contiguous leading to wrong memory locations, in case of 'it+1', because it will take the iterator to the next memory location, whereas 'it++' or '++it' will take the iterator to the location of the next iterator, that's why we use 'it++' or '++it' instead of 'it+1'.
#include <bits/stdc++.h>
using namespace std;
int main() {
// iterators point to the elements of a container.
vector<int> v = { 2,3,6,7 };
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << "\n";
vector<int> ::iterator it = v.begin(); // v.begin() points to v[0]
cout << (*(it + 1)) << endl; // prints the value 3 from v.
for (it = v.begin(); it != v.end(); ++it) { //v.end() points to the next position of the last element
cout << *(it) << endl;
}
// in case of iterators there's a difference between 'it++' and 'it+1'
/*
For vectors it's the same because vector has contiguous memory allocation,
but in case of maps and sets, the memory allocation couldn't be contiguous leading
to wrong memory locations, in case of 'it+1', because it will take the iterator
to the next memory location, whereas 'it++' or '++it' will take the iterator to the
location of the next iterator, that's why we use 'it++' or '++it' instead of
'it+1'.
*/
vector<pair<int, int>> v_p = {
{1,2}, {2,3}, {3,4},{4,5}
};
vector<pair<int, int>> ::iterator it1;
// 'it1' is an iterator that will point to the pairs in the vector
for (it1 = v_p.begin(); it1 != v_p.end(); it++) {
cout << (it1)->first << " " << it1->second << endl;
}
for (it1 = v_p.begin(); it1 != v_p.end(); it++) {
cout << (*it1).first << " " << (*it1).second << endl;
}
return 0;
}
- 'auto' keyword assumes the data type of a variable automatically.
Range based loops can iterate over the elements directly. It eliminates the need to dereference an iterator to get the value.
- Both of them could be used together in case of containers to make the code concise and neat.
#include <bits/stdc++.h>
using namespace std;
// iterators short forms and range based loops
int main() {
// 'auto' keyword -> it assumes/finds the data type of the variable automatically.
auto a = 1.0; // 'a' is assumed to be float now.
// in case of iterators it is particularly more useful because we don't have to write
// the whole syntax of iterators.
vector<int> v = { 2,3,6,7 };
// conditional loop
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
// vector<int> ::iterator it;
// for (it = v.begin(); it < v.end(); it++) {
// cout << *(it) << " ";
// }
// cout << endl;
// we can use auto here, instead of defining the iterator directly 'auto' can be used in the for loop
for (auto it = v.begin(); it != v.end(); it++)
{
cout << *(it) << " ";
}
cout << endl;
//range based loops directly iterate over elements and could be used on all containers
for (auto el : v) { // right now, 'el' is the copy of values in 'v'
el++; //-> this line will have no effect on the elements of 'v'
}
for (auto& el : v) {
el++; // this will actually change the values of 'v' as it is a reference variable
}
for (auto el : v) {
cout << el << " ";
}
cout << endl;
vector<pair<int, int>> v_p = { {1,2}, {2,3},{3,4} };
for (auto& value : v_p) {
cout << value.first << " " << value.second << endl;
}
return 0;
}
- Maps : stores key-value pairs
- Normal maps(ordered maps) : sorted order -> implemented using Red Black Trees
- Unordered maps : unsorted order.
*(iterator+1) cannot be done
, as there's no continguous memory allocation- Maps(unordered/ordered) contain unique keys only, if we re-assign a value to the key of a map, it will change the value of that key then.
- Worst case, Best Case and Average Case all have O(logN), however it may worsen in case of strings of long length as they need to lexicographically put in the map, then it will depend on string length too
#include <bits/stdc++.h>
using namespace std;
/*
1. maps : stores key-value pairs
2. normal maps : sorted order -> implemented using Red Black Trees
3. unordered maps : unsorted
4. iterator+1 cannot be done, as there's no continguous memory allocation
5. maps(normal/ordered ones obviously) contain unique keys only, if we re-assign a value to the key
of a map it will change the value of that key then.
*/
int main() {
map<int, string> m;
m[1] = "abc"; // 1 is the key and "abc" is its value
m[5] = "cdc"; // insertion and accessing takes O(logN) because of sorting
m[3] = "acd";
m[6]; // even this takes O(logN), and default value is empty string for string type values,
// and for 'int' and 'float' its 0.
map <int, string> ::iterator it; // iterator for map, inside its a pair.
for (it = m.begin(); it != m.end(); it++) {
// 'it < map.end()' doesn't make sense as there's no continuous memory location
cout << it->first << " " << it->second << endl;
// or cout << (*it).first << " " << (*it).second << endl;
}
// using auto keyword
for (auto& el : m) {
cout << el.first << " " << el.second << endl;
}
// these loops take N(logN) time complexity, as accessing take logN time and there's a loop which runs N times.
auto iter = m.find(3); // O(logN) time complexity.
if (iter == m.end()) {
cout << "No Value";
}
else {
cout << iter->first << " " << iter->second << endl;
}
// returns m.end() if the value is not found.
m.erase(3); // takes key as well as iterator's value as well
auto iter1 = m.find(4);
if (iter1 != m.end()) {
m.erase(iter1);
}
m.clear(); // clears the whole map
map<string, string> m1;
// insertion time complexity depends on the keys as well.
// so this time lexicographical comparison will happen and it will become s.size() * O(logN);
m1["abcd"] = "abc";
return 0;
}
Given N strings, print unique strings in lexicographical order with their frequency.
- Constraints:
- N <= 105
- s.length <= 100
#include <bits/stdc++.h>
using namespace std;
int main() {
int N;
cin >> N;
map<string, int> m; // map of strings and its frequency
for (int i = 0; i < N; i++) {
string s;
cin >> s;
if (!s.empty()) m[s]++;
}
for (auto& el : m) {
cout << el.first << " " << el.second << endl;
}
return 0;
}
-
Input
8
abc
def
abc
ghj
jkl
ghj
ghj
abc
-
Output
abc 3
def 1
ghj 3
jkl 1
- Worst case : O(n)
Use unordered maps wherever possible to reduce time complexity.
- Can't use complex data types like pairs as keys because the hash functions aren't defined internally.
- Basic data types are
int
,float
,string
etc. can be used.
#include <bits/stdc++.h>
using namespace std;
// all functions of ordered and unordered maps are same
int main() {
unordered_map<int, string> m;
// 1. inbuilt implementation -> uses hashing and hashtables
// 2. time complexity -> O(1) is average.
// 3. valid keys datatype -> complex datatype can't be keys
// unordered_map<pair<int, int>, string> m1; -> can't compile
// because hash functions of these things aren't internally defined
m[1] = "abc";
m[3] = "bce";
//Example: given Q queries and N strings, print the frequency of the strings given in each query.
unordered_map<string, int> m1;
int N;
cin >> N;
for (int i = 0; i < N; i++) {
string s;
cin >> s;
m1[s]++; // accessing and insertion is O(1)
}
int Q;
cin >> Q;
while (Q--) { //total time complexity for this loop is q*(length of string)
string s;
cin >> s;
cout << m1[s] << endl; // accessing and insertion is O(1)
}
// multimaps
multimap<string, int> mm;
// Insert elements
mm.insert({ "Alice", 90 });
mm.insert({ "Bob", 85 });
mm.insert({ "Alice", 95 });
// Accessing elements
auto range = mm.equal_range("Alice");
for (auto it = range.first; it != range.second; ++it) {
cout << it->first << ": " << it->second << endl;
}
return 0;
}
- Multimaps could be used to store multiple values with a single key. Not frequently used atleast in CP and we can use ordered maps with vector<int/string> as the value to store multiple instances.
- size: to get the size of sets and maps(ordered, unordered and multi)
- Use them whenever you need to remove duplicate(or some form of duplicacy) elements.
#include <bits/stdc++.h>
using namespace std;
void print_set(set<string>& s) {
for (auto& el : s) {
cout << el << " ";
}
cout << endl;
}
int main() {
// ordered set: Everything is like map's time complexity, i.e., O(logN).
// sets only contain unique elements
set<string> s;
s.insert("abc");
s.insert("zsd");
s.insert("bcd");
// s["abc"] -> it doesn't exist, we use '.find()' method for this.
auto it = s.find("abc"); // gives iterator if it exists or returns end iterator if it doesn't.
s.erase("abc"); // it also takes iterators
if (it != s.end()) s.erase(it);
s.clear(); // clears the whole set
return 0;
}
- Example
- Given N strings, print unique strings in lexicographical order.
- Constraints: N <= 105, string.length=100000
#include <bits/stdc++.h>
using namespace std;
int main() {
set<string> s;
int N;
cin >> N;
for (int i = 0; i < N; i++) {
string str;
cin >> str;
s.insert(str);
}
for (auto& el : s) {
cout << el << endl;
}
return 0;
}
- Example : Sets-STL
#include <bits/stdc++.h>
using namespace std;
int main() {
int q;
cin >> q;
set<int> s;
while (q--)
{
int y, x;
cin >> y >> x;
if (y == 1) {
s.insert(x);
}
else if (y == 2)
{
auto it = s.find(x);
if (it != s.end()) s.erase(it);
}
else if (y == 3)
{
auto it = s.find(x);
if (it != s.end()) {
cout << "Yes" << endl;
}
else {
cout << "No" << endl;
}
}
}
s.clear();
return 0;
}
- Functions are same like ordered set, just the time complexity is different.
- Similar to unordered maps, complex data types like pairs/sets/vectors can't be keys.
- Following is the example and code for unordered set:
#include <bits/stdc++.h>
using namespace std;
// Given N strings and Q queries, print yes if the string is present in the query. N <= 10^5, string.length=100000
int main() {
unordered_set<string> us;
int N;
cin >> N;
for (int i = 0; i < N; i++) {
string s;
cin >> s;
us.insert(s);
}
int Q;
cin >> Q;
while (Q--)
{
string s;
cin >> s;
auto it = us.find(s);
if (it != us.end()) {
cout << "yes" << endl;
continue;
}
cout << "no" << endl;
}
return 0;
}
-
(Unique Number of Occurences)[https://leetcode.com/problems/unique-number-of-occurrences/description/]
- unordered_sets; s.insert(2) returns a pair, in which first value is the iterator to that element in set and second is true/false depending on if the element was inserted.
#include <bits/stdc++.h>
using namespace std;
bool uniqueOccurrences(vector<int>& v) {
unordered_map<int, int> f;
for (auto& it : v) {
f[it]++;
}
unordered_set<int>o;
for (auto& el : f) {
if (!o.insert(el.second).second) {
return false;
}
}
return true;
}
int main() {
return 0;
}
- Allows multiple keys in the set in sorted manner.
#include <bits/stdc++.h>
using namespace std;
int main(){
multiset<string> s;
s.insert("abc"); // O(logN)
s.insert("bcd");
s.insert("abc");
s.insert("abc");
s.insert("zsdf");
auto it = s.find("abc"); // O(logN), returns the iterator of the first instance it finds, else it returns 's.end()'
if(it!=s.end()) s.erase(it); // deletes just the first instance of "abc",
// which is being pointed by the iterator.
s.erase("abc"); // deletes all instances of abc from the set.
return 0;
}
Example : The Monk and Class Marks
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
multiset<pair<int, string>, function<bool(pair<int, string>, pair<int, string>)>> m(
[](const pair<int, string>& a, const pair<int, string>& b) {
if (a.first != b.first) {
return a.first > b.first;
}
else {
return a.second < b.second;
}
}
);
for (int i = 0; i < n; i++) {
string name;
int marks;
cin >> name >> marks;
m.insert({ marks, name });
}
for (auto& p : m) {
cout << p.second << " " << p.first << endl;
}
m.clear();
return 0;
}
- Example: Monk and the Magical Candies
#include <bits/stdc++.h>
using namespace std;
int main() {
int t;
cin >> t;
while (t--)
{
long long n, k;
cin >> n >> k;
multiset<long long> m;
for (int i = 0; i < n; i++) {
long long x;
cin >> x;
m.insert(x);
}
long long sum = 0;
for (int i = 0; i < k; i++) {
auto it = --m.end();
sum += *it;
m.erase(it);
m.insert((*it) / 2);
}
cout << sum << endl;
m.clear();
}
return 0;
}
- Only ordered sets and maps can be nested with complex data structures which could be compared somehow.
#include <bits/stdc++.h>
using namespace std;
int main() {
map<pair<string, string>, vector<int>> m;
int n;
cin >> n;
for (int i = 0; i < n; i++) {
string fn, ln;
int ct;
cin >> fn >> ln >> ct;
for (int j = 0; j < ct; j++) {
int x;
cin >> x;
m[{fn, ln}].push_back(x);
}
}
for (auto& pr : m) {
auto& full_name = pr.first;
auto& list = pr.second;
cout << full_name.first << " " << full_name.second << endl;
cout << list.size() << endl;
for (auto& el : list)
{
cout << el << " ";
}
cout << endl;
}
return 0;
}
-
Stack works on LIFO principle and the most handy functions it provides are as follows:
- top: to retreive(not remove) the topmost element
- push: to push elements onto the stack
- pop: to pop the elements off the stack
- size: to check the size of the stack
-
Queue works on FIFO principle and the most common functions are:
- front : to retrieve(not remove) the first element in the queue
- push: to push elements in the back of the queue
- pop: to pop elements from the front of the queue
- size: to check the size of the queue
#include <bits/stdc++.h>
using namespace std;
int main() {
// Stack works on LIFO principle
stack<int> s; // could be any data structure ofc
s.push(1);
s.push(2);
s.push(3);
s.push(4);
cout<<s.size()<<endl; //gives the size of the stack
while (!s.empty())
{
cout << s.top() << endl;
s.pop();
}
// Queue works on FIFO principle
queue<string> q; // could be any data structure ofc
q.push("abc");
q.push("def");
q.push("ghi");
q.push("jkl");
cout<<q.size()<<endl; //gives the size of the queue
while (!q.empty())
{
cout << q.front() << endl;
q.pop();
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
bool isValid(string s) {
stack<char> st;
unordered_map<char, int> symbols = { {'[', -1}, {'(',-2}, {'{', -3}, {']', 1}, {')',2}, {'}', 3} };
for (char bracket : s)
{
if (symbols[bracket] < 0) {
st.push(bracket);
}
else {
if (st.empty()) return false;
char top = st.top();
st.pop();
if (symbols[top] + symbols[bracket] != 0) return false;
}
}
return st.empty();
}
// balanced brackets
int main() {
cout << isValid("()");
return 0;
}
#include <bits/stdc++.h>
using namespace std;
vector<int> NGE(vector<int>& v) {
stack<int> st;
vector<int> nge(v.size());
for (int i = 0; i < v.size(); i++) {
while (!st.empty() && v[i] > v[st.top()]) {
nge[st.top()] = i;
st.pop();
}
st.push(i);
}
while (!st.empty())
{
nge[st.top()] = -1;
st.pop();
}
return nge;
}
int main() {
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++) {
cin >> v[i];
}
vector<int> nge = NGE(v);
for (int i = 0; i < nge.size(); i++) {
cout << v[i] << " " << (nge[i] == -1 ? -1 : v[nge[i]]) << endl;
}
return 0;
}
sort()
function takes the startingaddress
and the(ending+1) address
and can take a comparator function as well.
- Sorting on vectors and arrays
#include <bits/stdc++.h>
using namespace std;
int main() {
//Introsort -> inbuilt sort algorithm = quick sort + heap sort + insertion sort
int n;
cin >> n;
// int a[n];
vector<int>a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// sort(a + 3, a + n); // takes starting address and (ending+1) address
sort(a.begin(), a.end());
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
- Sorting with comparator functions
#include <bits/stdc++.h>
using namespace std;
//comparator function for bubble sort
// bool should_i_swap(pair<int, int>a, pair<int, int> b) {
// if (a.first != b.first) {
// if (a.first > b.first) return true;
// }
// else {
// if (a.second < b.second) return true;
// }
// return false;
// }
//comparator function for inbuilt sort function
// bool should_i_swap(pair<int, int>a, pair<int, int> b) {
// if (a.first != b.first) {
// if (a.first > b.first) return false;
// }
// else {
// if (a.second < b.second) return false;
// }
// return true;
// }
// comparator function for introsort but in a simpler way
// return whatever you want the comparator function to do!
bool should_i_swap(pair<int, int>a, pair<int, int> b) {
if (a.first != b.first) {
return a.first < b.first;
}
return a.second > b.second;
}
int main() {
int n;
cin >> n;
vector<pair<int, int>>a(n);
for (int i = 0; i < n; i++) {
cin >> a[i].first >> a[i].second;
}
// Bubble sort in O(n^2)
// for (int i = 0; i < n; i++) {
// for (int j = i + 1; j < n; j++) {
// if (should_i_swap(a[i], a[j])) swap(a[i], a[j]);
// }
// }
sort(a.begin(), a.end(), should_i_swap); // return whatever you want the
// comparator function to do
for (int i = 0; i < n; i++) {
cout << a[i].first << " " << a[i].second << endl;
}
cout << endl;
return 0;
}
Upper and Lower Bounds using STL : Works in O(logN) on sorted Data Structures and in O(N) on unsorted ones
-
Returns the iterator to the first element in the range that is not less than (i.e., greater than or equal to) the specified value.
-
Takes the range of addresses (start: inclusive, end: exclusive) on which the operation needs to be performed, and the value of which the lower bound needs to be found.
-
If no element is greater than or equal to the specified value, it returns the end iterator of the range.
-
Lower Bound on Arrays:
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int a[n];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n); // works best on sorted data structures
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
int* ptr = lower_bound(a, a + n, 26); // finds the pointer/iterator of that value or the value just greater than that
if (ptr == (a + n)) {
cout << "Not Found" << endl;
}
else {
cout << (*ptr) << endl;
}
int* ptr_restricted = lower_bound(a + 1, a + n, 5); // range can be restrictive too
if (ptr_restricted == (a + n)) {
cout << "Not Found";
}
else {
cout << (*ptr_restricted) << endl;
}
return 0;
}
- Lower bound on vectors
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int>a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a.begin(), a.end());
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
auto it = lower_bound(a.begin(), a.end(), 26); // finds the pointer/iterator of that value or the value just greater than that
if (it == (a.end())) {
cout << "Not Found" << endl;
}
else {
cout << (*it) << endl;
}
auto it_restricted = lower_bound(a.begin(), a.end(), 5); // range can be restrictive too
if (it_restricted == (a.end())) {
cout << "Not Found";
}
else {
cout << (*it_restricted) << endl;
}
return 0;
}
- Lower bound on sets and maps
- Use set.lower_bound() function instead of regular lower_bound() function, same for maps as well.
- Regular lower_bound() works as O(N) in case of sets and maps but map.lower_bound() function works in O(logN) as it uses trees internally to traverse through the map.
- set.lower_bound() or map.lower_bound() just takes the value as input, and returns the iterator of either that value if its present in the set/map or of the first just greater element in it.
- map.lower_bound() works just on the keys of the map and returns the iterator of the value in the map.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
set<int> s;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
s.insert(x);
}
// auto it = lower_bound(s.begin(), s.end(), 5); // this is NOT O(logN) but O(N)
// for sets use set.lower_bound() function, as it uses tree implementation to traverse
auto it_set = s.lower_bound(5);
if (it_set != s.end()) cout << *it_set << endl;
map<int, int>m;
int k;
cin >> k;
for (int i = 0; i < k; i++) {
int x;
int y;
cin >> x >> y;
m[x] = { y };
}
auto it_map = m.lower_bound(3); // works on keys only!
if (it_map != m.end()) cout << (it_map)->first << " " << it_map->second << endl;
return 0;
}
-
Always returns the iterator of the first greater element than the current element. Works in the same way as lower bound.
-
Returns an iterator to the first element in the range that is greater than the specified value.
-
Takes the range of addresses (start: inclusive, end: exclusive) on which the operation needs to be performed, and the value of which the upper bound needs to be found.
-
If no element is greater than the specified value, it returns the end iterator of the range.
-
Example:
#include <bits/stdc++.h>
using namespace std;
int main(){
map<int, int>m;
int k;
cin >> k;
for (int i = 0; i < k; i++) {
int x;
int y;
cin >> x >> y;
m[x] = { y };
}
auto it_map_upper_bound = m.upper_bound(4);
if (it_map_upper_bound != m.end()) cout << it_map_upper_bound->first << " " << it_map_upper_bound->second << endl;
return 0;
}
- All of them take a range of starting(inclusive) and ending(exclusive) addresses to operate on.
- min_element() : returns the pointer/iterator of the minimum element in the range.
- max_element() : returns the pointer/iterator of the maximum element in the range.
- count() : counts the occurence of an element in the given range and returns the count.
- find() : returns the iterator/pointer of that element and if not found then returns the iterator/pointer of the next to the last element in the data structure(basically the end iterator/pointer), in the provided range.
- reverse() : reverses the elements of a data structure(like arrays, vectors and strings) in the provided range.
- accumulate(): sums the elements in the given range and takes the initial sum as the third argument as well.
- Obviously, in case of arrays, instead of iterators we directly use the variable name(say a) for ranges like this :
sort(a, a+n)
, asssuming that there are 'n' elements in the array.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++) {
cin >> v[i];
}
// all of these internal algorithms take up a range to operate in on a data structure.
int min = *min_element(v.begin(), v.end());
cout << min << endl;
int max = *max_element(v.begin(), v.end());
cout << max << endl;
int count_of_an_element = count(v.begin(), v.end(), 2);
cout << count_of_an_element << endl;
int sum = accumulate(v.begin(), v.end(), 0); // initial sum taken to be 0, if it is taken something else, it would be added to the final sum
cout << sum << endl;
auto it = find(v.begin(), v.end(), 6); //returns the iterator/pointer of that element, if not found then of the next to last element
if (it != v.end()) cout << *it << endl;
else cout << "Element Not Found" << endl;
// reverses the original data structure
reverse(v.begin(), v.end());
// comes in handy in case of arrays, vectors and strings especially
for (int i = 0; i < n; i++) {
cout << v[i] << " ";
}
cout << endl;
string s = "dfbhfew";
reverse(s.begin() + 2, s.end()); // reverses from 2nd index to the last
cout << s << endl;
return 0;
}
- all_of(): returns true only if all the elements satisfy the condition.
- any_of(): returns true if atleast one of the elements satisfy the condition.
- none_of(): returns true if none of the elements satisfy the condition.
#include <bits/stdc++.h>
using namespace std;
bool is_positive(int x){
return x>0;
}
int main() {
vector<int> v = { 2,4,6,7 };
//using regular function
cout << all_of(v.begin(), v.end(), is_positive) << endl;
//using lambda function
cout << any_of(v.begin(), v.end(), [](int x) {return x > 0;}) << endl;
cout << none_of(v.begin(), v.end(), [](int x) {return x > 0;}) << endl;
return 0;
}
-
This is a concise way of writing a function and could be assigned to a variable as well, like this:
-
Syntax :
[](int a, int b){ return a+b;}
, to call it, write parentheses just to the side of it like this :[](int a, int b){ return a+b;}()
;auto sum = [](int a, int b){ return a+b;};
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << [](int x, int y) {return x + y;}(2, 3) << endl; // calling the anonymous lambda function
auto sum = [](int x, int y) {return x + y;};
cout << sum(2, 5) << endl;
return 0;
}
-
Formal Definition of hashing: Hashing is a technique used to map data from a large input space to a smaller, fixed-size range of integers (often referred to as "hashes").
The goal is to efficiently store and retrieve data in a way that allows for quick lookups, insertions, and deletions.
Hashing is commonly used to implement data structures like hash tables, which arekey in solving problems involving frequent queries on large datasets.
-
Formal Definition of Pre-computation : Pre-computation refers to the
process of computing and storing results in advance before they are needed during the actual query or operation phase
. This technique is especially useful in scenarios where the same computation may need to be repeated multiple times. -
Here's an example: We reduced the time complexity by creating a hash array then did the pre-computation(in a frequency array and stored it) till the max size,i.e., 105 and simply looked up for the data in our hash vector.
-
This reduces the time complexity by a lot, especially for large datasets.
#include <bits/stdc++.h>
using namespace std;
/*
Given an array 'a' of N integers. Given Q queries and in each
query given a number X, print the count of that number in array.
Constraints:
1<=N<=10^5
1<=a[i]<=10^7
1<=Q<=10^5
*/
int main() {
int n;
cin >> n;
int a[n];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int q;
cin >> q;
// while (q--) // O(q*n) == O(10^10) => this won't run in one second
// {
// int x;
// cin >> x;
// int ct = 0;
// for (int i = 0; i < n; i++) {
// if (a[i] == x) {
// ct++;
// }
// }
// cout << ct << endl;
// }
// Pre-computing the frequencies of the element
vector<int> freq(1e7 + 10); // hash vector(or array)
for (int i = 0; i < n; i++) { // O(n)
freq[a[i]]++; // calculate frequency of each element in advance
}
while (q--) { // O(q)
int x;
cin >> x;
cout << freq[x] << endl;
}
// Total time complexity = O(n+q)
return 0;
}
#include <bits/stdc++.h>
using namespace std;
vector<int> twoSum(vector<int>& v, int target) {
unordered_map<int, int> m;
for (int i = 0; i < v.size(); i++) {
int val = target - v[i];
if (m.find(val) != m.end()) {
return { m[val], i };
}
m[v[i]] = i;
}
return { -1, -1 };
}
int main() {
int n, target;
cin >> n >> target;
vector<int>v(n);
for (int i = 0; i < n; i++) {
cin >> v[i];
}
vector<int> ans = twoSum(v, target);
for (int i = 0; i < ans.size(); i++) {
cout << ans[i] << " ";
}
return 0;
}
- Prefix sum on 1D arrays
- Use 1-based indexing the questions of prefix sum
- To find occurences or sums in a range.
#include <bits/stdc++.h>
using namespace std;
/*
Given an array 'a' of N integers. Given Q queries and in each query given L
and R, print sum of the array elements from L to R (both incluive).
Constraints:
1<=N<=10^5
1<=a[i]<=10^9
1<=Q<=10^5
1<=L,R<=N
*/
// Use 1-based indexing in case of prefix sum
int main() {
int N;
cin >> N;
vector<int> a(N + 1);
for (int i = 1; i <= N; i++) {
cin >> a[i];
}
// pre-compute the prefix-sum in a pf vector, declare it vector<long long>
// because of the order of elements of the vector 'a'
// computing the prefix sum
vector<long long> pf(N + 1); // 'long long' because of the nature of the elements of vector 'a'
pf[1] = a[0];
for (int i = 2; i <= N; i++) {
pf[i] = pf[i - 1] + a[i];
}
int q;
cin >> q;
while (q--) {
int l, r;
cin >> l >> r;
cout << pf[r] - pf[l - 1] << endl;
}
// O(N + Q)
return 0;
}
- Prefix sum on 2D arrays
#include <bits/stdc++.h>
using namespace std;
/*
Given 2D array of N*N integers. Given Q queries and in each query given
a,b,c and d print sum of square represented by (a,b) as top left point and
(c,d) as top bottom right point.
Constraints:
1<=N<=10^3
1<=a[i][j]<=10^9
1<=Q<=10^5
1<=a,b,c,d<=N
*/
int main() {
int n;
cin >> n;
vector<vector<int>>v(n + 1, vector<int>(n + 1, 0));
vector<vector<long long>>pf(n + 1, vector<long long>(n + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> v[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
pf[i][j] = v[i][j] + pf[i - 1][j] + pf[i][j - 1] - pf[i - 1][j - 1];
}
}
int q;
cin >> q;
while (q--)
{
int a, b, c, d;
cin >> a >> b >> c >> d;
cout << pf[c][d] - pf[a - 1][d] - pf[c][b - 1] + pf[a - 1][b - 1] << endl;
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
// L to R addition type problem
int main() {
int n;
cin >> n;
vector<long long> v(n + 1, 0);
int t;
cin >> t;
vector<vector<long long>> q(t, vector<long long>(3));
for (int i = 0; i < t; i++) {
for (int j = 0; j < 3; j++) {
cin >> q[i][j];
}
}
// Setting the elements cleverly
for (int i = 0; i < t; i++) {
int a = q[i][0], b = q[i][1], k = q[i][2];
v[a] += k;
if (b + 1 <= n) v[b + 1] -= k;
}
// performing prefix sum to pre-compute the answer
for (int i = 1; i <= n; i++) {
v[i] += v[i - 1];
}
long long max_el = v[1];
for (int i = 2; i <= n; i++) {
if (max_el < v[i]) {
max_el = v[i];
}
}
cout << max_el;
return 0;
}
- Prefix Sum and Hashing both
#include <bits/stdc++.h>
using namespace std;
// find in the range L to R if the string is palindromic?
/*
Input:
2
5 5
abcec
1 2
2 5
3 5
1 5
1 4
5 5
aabbc
1 2
2 5
3 5
1 5
1 4
Output:
NO
NO
YES
NO
NO
YES
NO
YES
YES
YES
*/
// prefix sum and hashing
int main() {
int t;
cin >> t;
while (t--)
{
int n, q;
cin >> n >> q;
string s;
cin >> s;
// hashing
vector<vector<int>>v(s.size() + 1, vector<int>(26, 0));
for (int i = 0; i < n; i++) {
v[i + 1][s[i] - 'a']++;
}
// prefix sum
for (int i = 0; i < 26; i++) {
for (int j = 1; j <= n; j++) {
v[j][i] += v[j - 1][i];
}
}
while (q--)
{
int l, r;
cin >> l >> r;
int oddCt = 0;
for (int i = 0; i < 26; i++) {
int charCt = v[r][i] - v[l - 1][i];
if (charCt % 2 != 0) oddCt++;
}
if (oddCt > 1) cout << "NO\n";
else cout << "YES\n";
}
}
return 0;
}
x = p1n1 ×p2n2 ×...ptnt, where pk is a prime factor of the number x.
- Count of divisors = (n1 + 1) × (n2+1) × (n3+1)... × (nt+1)
- Sum of divisors = ((p1n1+1)/(p1-1)) + ((p2n2+1)/(p2-1)) + ....
- Finding all the factors of a number, their count and sum as well
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int ct = 0; // count of divisors
int sum = 0; // sum of divisors
/* 1. naive approach : O(N) method
for (int i = 1; i <= n; i++) {
if (n % i == 0) cout << i << endl;
}
*/
// 2. O(sqrt(n)) method
for (int i = 1; i * i <= n; i++) {
if (n % i == 0) {
cout << i << " " << n / i << "\n";
ct++;
sum += i;
if (n / i != i) {
ct++;
sum += n / i;
}
}
}
cout << "ct: " << ct << ", sum: " << sum << "\n";
return 0;
}
- Checking if a number is prime or not
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
bool is_prime = true;
// 1. O(N) - naive approach
/* for (int i = 2; i < n; i++) {
if (n == 1) {
is_prime = false;
break;
}
if (n % i == 0) {
is_prime = false;
break;
}
} */
//2. O(sqrt(N))
for (int i = 2; i * i <= n; i++) {
if (n == 1) {
is_prime = false;
break;
}
if (n % i == 0) {
is_prime = false;
break;
}
}
cout << is_prime << "\n";
return 0;
}
- Finding all the prime factors of a number
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> prime_factors;
// 1. O(N) approach
/* for (int i = 2; i <= n; i++) {
while (n % i == 0) {
prime_factors.push_back(i);
n /= i;
}
} */
// 2. O(sqrt(N)) approach
for (int i = 2; i * i <= n; i++) {
while (n % i == 0) {
prime_factors.push_back(i);
n /= i;
}
}
if (n > 1) { // the last prime number which would be left will be caught by this
prime_factors.push_back(n);
}
for (int prime : prime_factors) {
cout << prime << " ";
}
return 0;
}
- A form of
pre-computation
in a given range.
// pre-compute all the numbers
#include <bits/stdc++.h>
using namespace std;
const int N = 1e7 + 10;
vector<bool> isPrime(N, 1);
int main() {
isPrime[0] = isPrime[1] = false;
// sieve algorithm -> precompute all the primes in the given range
// O(NloglogN) -> time complexity, smaller than O(logN)
for (int i = 2; i < N; i++) {
if (isPrime[i] == true) {
for (int j = 2 * i; j < N; j += i) {
isPrime[j] = false;
}
}
}
for (int i = 1; i < 100; i++) {
cout << i << ": " << (isPrime[i] == true ? "true" : "false") << "\n";
}
return 0;
}
-
Number of digits in a binary number = (int)(log2(N)) + 1.
-
n & (-n) isolates the rightmost set bit of n. It doesn't give the position but rather a number where only the rightmost set bit of n is set.
Example: let n = 10010, then (10010) & (01110) = 00010, here 01110 is the 2's complement(negative) of 10010.
-
rightmost_unset_bit = (~n) & (n + 1). rightmost_unset_bit is a value that has only one bit set at the position of the rightmost unset bit in n.
#include <bits/stdc++.h>
using namespace std;
int setBit(int n) {
if (n == 0) return 1;
int rightmost_unset_bit = (~n) & (n + 1);
return n | rightmost_unset_bit;
}
int main() {
int n;
cin >> n;
cout << setBit(n);
return 0;
}
-
n & (n-1) clears the rightmost set bit.
Example: let n = 10010, then (10010) & (10010-1) = (10010) & (10001) = 10000
-
(1 << i) & n tells if the ith bit is set in a number or not. If 1-based indexing then (1<<(i-1)) & n would give the same result.
-
1 << i = 2i, it would overflow after the 31st bit, as the limit is 231-1. Left shifting a number means multiplying by 2.
-
1 >> i = 2-i. Right shifting a number means dividing by 2.
-
1 ^ bit = flipped bit.
- 1 ^ 0 = 1
- 1 ^ 1 = 0
-
1 & bit = bit.
- 1 & 1 = 1
- 1 & 0 = 0
-
1 | bit = 1
- 1 | 1 = 1
- 1 | 0 = 1
-
0 & bit = 0 → This is used for masking unwanted bits and set them to 0.
-
Find sum of set bits upto a number N
int countSetBits(int n) {
int count = 0;
for (int i = 0; (1 << i) <= n; ++i) {
int bitMask = (1 << (i + 1)); // Size of full cycles
int completeCycles = n / bitMask; // Number of complete cycles
int remaining = n % bitMask; // Remaining numbers after full cycles
count += completeCycles * (bitMask / 2) + max(0, remaining - (bitMask / 2) + 1);
}
return count;
}
- Dividing two integers using bit manipulation !!!IMPORTANT!!!
#include <bits/stdc++.h>
using namespace std;
int divide(int dividend, int divisor) {
bool is_negative = (dividend < 0) ^ (divisor < 0);
if (divisor == -1) {
if (dividend == INT_MIN) return INT_MAX;
return -dividend;
}
if (dividend == 0)
return 0;
if (dividend == divisor)
return 1;
if (divisor == 1)
return dividend;
long long q = 0;
long long d1 = abs(divisor); // divisor
long long d2 = abs(dividend); // dividend
// what we need to find is d2/d1
while (d2 >= d1) {
long long temp = d1, multiple = 1;
while (d2 >= (temp << 1)) {
temp <<= 1;
multiple <<= 1;
}
d2 -= temp;
q += multiple;
}
if (is_negative) {
q = -q;
}
if (q > INT_MAX) {
return INT_MAX;
}
if (q < (INT_MIN)) {
return INT_MIN;
}
return q;
}
int main() {
int dividend, divisor;
cin >> dividend >> divisor;
cout << divide(dividend, divisor);
return 0;
}
- XOR from L To R, both inclusive, i.e., [L,R]
#include <bits/stdc++.h>
using namespace std;
int xorUpto(int a) { // xor from 0 to a(both inclusive)
switch (a % 4) {
case 0: return a;
case 1: return 1;
case 2: return a + 1;
case 3: return 0;
}
return 0;
}
int xor_from_l_to_r(int l, int r) { // both inclusive
return xorUpto(l - 1) ^ xorUpto(r);
}
int main() {
int l, r;
cin >> l >> r;
cout << xor_from_l_to_r(l, r);
return 0;
}
- Binary Search
- Time Complexity: O(log(n))
#include <bits/stdc++.h>
using namespace std;
int main(){
int n, target;
cin >> n >> target;
int v[n];
int start =0, end = n-1;
while(start<=end){
int mid = start + (end-start)/2;
if(v[mid]==target){
return mid;
}else if(v[mid]<target){
start = mid+1;
}else{
end = mid-1;
}
}
return -1;
}
- Functions which are either increasing or decreasing and NOT both.
- Functions which return a
boolean
and change the nature once at a point. - Example:
- Suppose some function is in a given range x1 to x2 and suppose there's a point x0, in the range x1 to x2, then if the function returns just
true
till x0 and then returnsfalse
after it or vice-versa is called a predicate function.
- Suppose some function is in a given range x1 to x2 and suppose there's a point x0, in the range x1 to x2, then if the function returns just
- Floor of a number
- It's the largest number less than or equal to the given target.
#include <bits/stdc++.h>
using namespace std;
int main() {
// works for sorted array ofc
int n, target;
cin >> n >> target;
int v[n];
for (int i = 0; i < n; i++) {
cin >> v[i];
}
int start = 0, end = n - 1, f = -1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (v[mid] == target) {
f = mid;
break;
} else if (v[mid] > target) {
end = mid - 1;
} else {
f = mid; // potential floor
start = mid + 1;
}
}
cout << f << (f == -1 ? -1 : v[f]) << "\n";
return 0;
}
- Ceil of a number
- It's the smallest number greater than or equal to the given target.
#include <bits/stdc++.h>
using namespace std;
int main() {
// works for sorted array ofc
int n, target;
cin >> n >> target;
int v[n];
for (int i = 0; i < n; i++) {
cin >> v[i];
}
int start = 0, end = -1, c = -1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (v[mid] == target) {
c = mid;
break;
} else if (v[mid] < target) {
start = mid - 1;
} else {
c = mid; // potential ceil
end = mid - 1;
}
}
cout << c << " " << (c == -1 ? -1 : v[c]);
return 0;
}
- Find the first and last occurence of a number
- To find the first occurrence always break the loop with
start = mid+1
. - To find the last occurrence always break the loop with
end = mid-1
.
- To find the first occurrence always break the loop with
#include <bits/stdc++.h>
using namespace std;
// to get the last position break the loop with start=mid+1
// to get the first position break the loop with end = mid-1
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> v(2, -1);
int n = nums.size();
int start = 0, end = n - 1;
int ans = -1;
// finds first occurence
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
ans = mid;
start = mid + 1; // always break it with start=mid+1;
} else if (nums[mid] > target) {
end = mid - 1;
} else {
start = mid + 1; // always break it with start=mid+1;
}
}
v[1] = ans;
start = 0;
end = n - 1;
ans = -1;
//finds last occurence
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
ans = mid;
end = mid - 1; // always break it with end = mid-1;
} else if (nums[mid] < target) {
start = mid + 1;
} else {
end = mid - 1; // always break it with end = mid-1;
}
}
v[0] = ans;
return v;
}
int main() {
return 0;
}
- Here we will use the concept of predicate function and monotonic functions.
- We create an
is_possible
predicate function which gives us abool
as an output till a certain point and reverses it's nature for the remaining range, i.e.,true
for a given range and thenfalse
for the remaining range or vice-versa. start = mid+1
orend = mid-1
is set accordingly if we want to find the firsttrue
value or the firstfalse
value or the lasttrue
value or the lastfalse
value, depending on the question.
- Finding square root of a number
#include <bits/stdc++.h>
using namespace std;
class Solution {
private:
bool is_possible(long long int x, long long int mid) {
return mid * mid <= x;
}
public:
int mySqrt(int x) {
long long int ans = -1;
long long int start = 0;
long long int end = x;
while (start <= end) {
long long int mid = start + (end - start) / 2;
if (is_possible(x, mid)) {
ans = mid;
start = mid + 1;
} else {
end = mid - 1;
}
}
return ans;
}
};
int main() {
return 0;
}
class Solution {
private:
bool is_possible(vector<int>& piles, int h, int mid) {
int n = piles.size();
for (int i = 0; i < n; i++) {
if (mid >= piles[i]) {
h--;
} else {
if (piles[i] % mid == 0) {
h -= piles[i] / mid;
} else {
h -= (piles[i] / mid) + 1;
}
}
}
return h>=0;
}
public:
int minEatingSpeed(vector<int>& piles, int h) {
int start = 1;
int end = *max_element(piles.begin(), piles.end());
int ans = -1;
while(start<=end){
int mid = start + (end-start)/2;
if(is_possible(piles, h, mid)){
ans = mid;
end = mid-1;
}else{
start = mid+1;
}
}
return ans;
}
};
#include <bits/stdc++.h>
using namespace std;
class Solution {
private:
bool is_possible(vector<int>& v, int threshold, int mid) {
long long int n = v.size();
long long int sum = 0;
for (int i = 0; i < n; i++) {
if (v[i] % mid == 0) {
sum += v[i] / mid;
} else {
sum += v[i] / mid + 1;
}
}
if (sum <= threshold) return true;
return false;
}
public:
int smallestDivisor(vector<int>& nums, int threshold) {
long long int start = 1;
long long int end = *max_element(nums.begin(), nums.end());
long long int ans = -1;
while (start <= end) {
long long int mid = start + (end - start) / 2;
if (is_possible(nums, threshold, mid)) {
ans = mid;
end = mid - 1;
} else {
start = mid + 1;
}
}
return ans;
}
};
int main() {
return 0;
}
int findKthPositive(vector<int>& v, int k) {
int start = 0, end = v.size() - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
int missing = v[mid] - (mid + 1);
if (missing < k) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return start + k;
}
- Whenever you get the problem of finding max/min substring think of two pointer and sliding window approach.
- These algos are more of a constructive approach and the only thing which remains constant is the window and it keeps moving across the string.
- How the window is moved, depends on the problem.
-
We start out with an empty window, i.e.,
l and r
would be at the same position then we keep expanding the window until some condition is violated and at that point we simply move the window ahead, i.e., movel
to a new position. -
l
: moves the window,r
expands the window.
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
int n = s.size();
vector<int>m(256, -1); // hash array for 250 characters
int l = 0, r = 0;
int max_length = 0;
while (r < n) {
int length = 0;
if (m[s[r]] != -1) { // check if the character is already in the hash array
if (m[s[r]] >= l) { // check if that character is in our window
l = m[s[r]] + 1; // move the window if it is in the window
}
}
m[s[r]] = r; //re-assign new value to the position of that character
length = r - l + 1;
max_length = max(max_length, length);
r++;
}
cout << "max length: " << max_length;
return 0;
}
-
It breaks large problems into smaller ones, and once we devise a solution for the smaller one then it eventually adds upto the solution of large problems.
-
To explore all the possibilities → use recursion.
-
To count the number of ways.
-
If there are multiple ways of doing something, then minimum or maximum number of ways are asked.
-
Try to write the function in its own terms. For example : digit_sum(num)
-
digit_sum(num) = last_digit + digit_sum(num_with_its_last_digit_removed);
Let's say, num = 1234.
digit_sum(1234) = 4 + digit_sum(123);
digit_sum(123) = 3 + digit_sum(12);
digit_sum(12) = 2 + digit_sum(1);
digit_sum(1) = 1 + digit_sum(0);
digit_sum(0) = 0; -> base condition to terminate.
-
Hence, digit_sum(num) = (num%10) + digit_sum(num/10)
-> from the above pattern.
#include <bits/stdc++.h>
using namespace std;
int digit_sum(int num)
{
if (num == 0)
return 0;
return (num % 10) + digit_sum(num / 10);
}
int main()
{
int num;
cin >> num;
cout << "Hello world";
cout << num;
// this is the recursive part when you go down the tree
digit_sum(num);
// this is the part when you come back up in the tree
return 0;
}
Compute the total number of function calls.
Compute the time complexity of each function.
-
In the above example, for each digit the function call happens once, so the total number of function calls = number of digits(say, n). In each function call, the operations have O(1) time complexity as there's no loop, rather just a simple if statement and a function call.
Hence the total time complexity is given by : O(1 * n) = O(n)
-
If the number is taken to be 'n', then the number of digits will be: log(n) + 1. Then the time complexity will be : O(log(n) * 1) = O(log(n)).
-
Less significant terms and constants are ignored in the Big-O notation. (That's why O(log(n)) in the above example.)
-
Backtracking is simply used to maintain the state of recursion, i.e., for the same function when the recursion comes back up to it from the stack then the variables in the body/parameters need to be same.
-
For example, when we pass some string/array using reference then the actual string/array is being modified in each function call hence when the recursion comes back up in the tree to run some code below the call then the actual string/array for that function would have been modified which would result in unexpected behavior. So to reset it to the original state, i.e., to the state in which the recursion started for that string/array we revert the changes.
This preserves the actual state of the recursion and allows us to traverse through the recursive tree properly.
-
Maze traversal is the best example for this.
-
Or when we pass some array/string/variable(by reference) and we need to come back up in the function to perform some operation on it then we might need to use backtracking if the operations are needed to be performed on the previous version of the variable, i.e., the one with which the recursion was called.
-
It's nothing different from recursion, just a fancy name.
-
Variable number of recursive calls at a single level → use
for loop
and put the recursion inside it. -
Use the
void
return type to explore all the possibilities. -
Return
1
or0
if you want to count then add all the recursive calls and return them. Return0
when the condition isn't satisified and if it's satisified return1
. -
Return
true
orfalse
if you need to break the recursion after the condition isonce satisfied
. Returntrue
for satisified conditions else returnfalse
.
-
Unique Elements in an array: Use the
take/not-take technique
, nofor loop
will be required and terminate the recursion using the base case/termination conditions yourself.- Entirely Based on
take/not-take
technique: Generate all binary strings - Taking the same element unlimited number of times
- Taking an element only once
- Entirely Based on
-
Duplicate Elements in an array: Sort the array first, use the
for loop
and inside it put the recursion,for loop
will automatically do thetake/not-take
and will implicitly terminate(whenfor loop
condition will eventually be unsatisfied), so we don't necessarily need a base case to terminate the recursion for preventing stack overflow because of infinite loop condition, rather terminate it when the conditions are satisfied.- Taking the element only once and skipping the duplicates
- Take an element unlimited number of times then simply remove the duplicate ones and solve it like Combination Sum I
-
Permutations: We use the for loop method because of the variable number of choices to start with and put the recursion inside the for loop which eventually picks up the elements provided the same element hasn't been picked already. If there are duplicate elements use the way taught in Combination Sum II and add the
f[i-1]==0
along with it.
class Solution{
private:
void fn(int n, int i, string &ds, vector<string>&ans){
if(i==n){
ans.push_back(ds);
return;
}
if(i>0 && ds[i-1]=='0'){
// take '0'
ds.push_back('0');
fn(n, i+1, ds, ans);
ds.pop_back();
// not take '0', i.e., take '1'
ds.push_back('1');
fn(n, i+1, ds, ans);
ds.pop_back();
}else if(i>0 && ds[i-1]=='1'){
// cannot take '1' due to constraints
ds.push_back('0');
fn(n, i+1, ds, ans);
ds.pop_back();
}
}
public:
vector<string> generateBinaryStrings(int num){
vector<string> ans;
string ds;
// take '0'
ds.push_back('0');
fn(num, 1, ds, ans);
ds.pop_back();
// not take '0', i.e., take '1'
ds.push_back('1');
fn(num, 1, ds, ans);
ds.pop_back();
return ans;
}
};
- Remember this pattern that when we have to pick elements from an array which consists of unique elements then we use the
pick/not-pick
method without thefor loop
!
class Solution {
private:
void solve(vector<int>& candidates, int target, vector<vector<int>>& ans,
vector<int>& current, int n, int i) {
if (i >= n) {
if (target == 0) {
ans.push_back(current);
}
return;
}
// take
if (target > 0) { // important line
current.push_back(candidates[i]);
solve(candidates, target - candidates[i], ans, current, n, i);
current.pop_back();
}
// not take
solve(candidates, target, ans, current, n, i + 1);
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> current;
int n = candidates.size();
solve(candidates, target, ans, current, n, 0);
return ans;
}
};
- In the above question we stop when the target becomes negative, i.e, we can take the elements as many times as we want but if the target becomes negative we stop because then no combination of it would sum upto the target.
#include <bits/stdc++.h>
using namespace std;
void solve(vector<int>& candidates, int target, vector<vector<int>>& ans,
vector<int>& current, int n, int i) {
if (i >= n) {
if (target == 0) {
ans.push_back(current);
}
return;
}
// take
if (target > 0) { // important line
current.push_back(candidates[i]);
solve(candidates, target - candidates[i], ans, current, n, i+1);
current.pop_back();
}
// not take
solve(candidates, target, ans, current, n, i + 1);
}
int main(){
int n, target;
cin >> n >> target;
vector<int> ds;
vector<int> candidates(n);
for(int i=0; i<n; i++){
cin >> candidates[i];
}
vector<<vector<int>> ans;
solve(candidates, target, ans, ds, n, 0);
return 0;
}
- Example: Combination Sum III
class Solution {
private:
void fn(vector<int>&v, int target, int k,int i, vector<int>&ds, vector<vector<int>>&ans){
if(target<0) return;
// no need of the above condition, because if the target even becomes negative then also recursion will eventually stop as we are not taking the same element multiple times hence the third condition of if(i>=v.size()) will save us eventually unlike Combination Sum I.
if(ds.size()==k){
if(target==0){
ans.push_back(ds);
}
return;
}
if(i>=v.size()) return;
ds.push_back(v[i]);
target-=v[i];
fn(v, target, k, i+1, ds, ans);
target+=v[i];
ds.pop_back();
fn(v, target, k, i+1, ds, ans);
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
vector<int> v;
vector<int> ds;
vector<vector<int>> ans;
for(int i=0; i<9; i++){
v.push_back(i+1);
}
fn(v, n, k, 0, ds, ans);
return ans;
}
};
-
First sort the original array.
-
Remember this pattern, where when we have to choose
non-duplicate
elements from an array having duplicate elements ofcourse, then we usefor loop
along with thepick/not-pick
technique and put the recursion inside it, as done below. -
No explicit base condition
(like ind>=n) because thefor loop
does it implicitly anyway.
#include <bits/stdc++.h>
using namespace std;
void f(int ind, int target, int& n, vector<int>& ds,
vector<vector<int>>& ans, vector<int>& v) {
if (target == 0) {
ans.push_back(ds);
return;
}
// recursion inside the for loop and we moved ahead as we can't pick the same element unlimited number of times
for (int i = ind; i < n; i++) {
if (i > ind && v[i] == v[i - 1])
continue;
if (v[i] > target)
break;
ds.push_back(v[i]);
f(i + 1, target - v[i], n, ds, ans, v);
ds.pop_back();
}
}
int main() {
int n, target;
cin >> n, target;
vector<int> v;
vector<vector<int>> ans;
vector<int> current;
for (int i = 0; i < n; i++) {
cin >> v[i];
}
sort(v.begin(), v.end()); // sort the original array
f(0, target, n, current, ans, v);
return 0;
}
- We stop when the target becomes 0, that means we got the answer and we either store it or print it. To check for the duplicate elements we resort back to the for loop and
if(i>ind && v[i]==v[i-1])
we skip that iteration, if the target becomes negative/v[i]>target
we canbreak/return
the/from the loop.
- Similar to Combination Sum I
#include <bits/stdc++.h>
using namespace std;
void f(int ind, int sum, int n, vector<int>& v, vector<int>& current) {
if (ind >= n) {
if (current.size() == 0) cout << "{}";
for (auto& it : current) {
cout << it << " ";
}
cout << "\n" << "sum: " << sum << "\n \n";
return;
}
current.push_back(v[ind]);
f(ind + 1, sum + v[ind], n, v, current);
current.pop_back();
f(ind + 1, sum, n, v, current);
}
int main() {
int n;
cin >> n;
vector<int> v(n);
vector<int> current;
for (int i = 0; i < n; i++) {
cin >> v[i];
}
f(0, 0, n, v, current);
return 0;
}
-
Similar to Combination Sum II
-
Here we don't need fully formed subsets till
n
rather we want all the unique subsets, that's whyno explicit base condition
because thefor loop
does it implicitly anyway.
#include <bits/stdc++.h>
using namespace std;
void f(int ind, int n, vector<int>& v, vector<int>& current) {
// Include the current subset in the output immediately
for (auto& it : current) {
cout << it << " ";
}
cout << "\n";
// Explore further elements starting from the current index
for (int i = ind; i < n; i++) {
if (i > ind && v[i] == v[i - 1]) {
continue; // Skip duplicates
}
current.push_back(v[i]);
f(i + 1, n, v, current);
current.pop_back();
}
}
int main() {
int n;
cin >> n;
vector<int> v(n);
vector<int> current;
for (int i = 0; i < n; i++) {
cin >> v[i];
}
sort(v.begin(), v.end()); // Sort to handle duplicates
f(0, n, v, current);
return 0;
}
- Approach 1
- We go until we have all the elements in the data structure.
- Remember this pattern and there's for loop and inside it the recursion because of variable number of function calls at each step.
i=0
in each case because again for every element previous elements need to be considered.
#include <bits/stdc++.h>
using namespace std;
void f(int ind, int n, vector<int>& v, int freq[], vector<int>& ds) {
if (ds.size() == n) { // we will go till we have all the three elements
for (auto& i : ds) {
cout << i << " ";
}
cout << endl;
return;
}
for (int i = 0; i < n; i++) { // for each element I have to check from i=0 only
// like in case of [1,2,3] if we take 2 first then [2, 1, 3] is also possible, hence we need i=0.
if (!freq[i]) {
freq[i] = 1;
ds.push_back(v[i]);
f(i + 1, n, v, freq, ds);
ds.pop_back();
freq[i] = 0;
}
}
}
int main() {
int n;
cin >> n;
vector<int> v(n);
vector<int> current;
for (int i = 0; i < n; i++) {
cin >> v[i];
}
int freq[n] = { 0 };
f(0, n, v, freq, current);
return 0;
}
- We skip it just like we did it in Subets II and Combination Sum II.
#include <bits/stdc++.h>
using namespace std;
void permutations(int j, vector<int>& v, vector<int>& ds, int f[]) {
if (ds.size() == v.size()) {
for (auto&& it : ds) {
cout << it << " ";
}
cout << "\n";
}
for (int i = 0; i < v.size(); i++) {
if (i > 0 && v[i] == v[i - 1] && f[i - 1] == 0) {
continue;
}
if (f[i] != 1) {
f[i] = 1;
ds.push_back(v[i]);
permutations(i + 1, v, ds, f);
ds.pop_back();
f[i] = 0;
}
}
}
int main() {
vector<int> v = { 1,2,2 };
vector<int> ds;
int f[v.size()] = { 0 };
permutations(0, v, ds, f);
return 0;
}
- Note: Using IBH technique taught by Aditya Verma THE MAN
- Recursive function wo kaam kar dega tum toh ye socho ki kaise sambhaloge use.
class Solution{
private:
void insert_at_bottom(stack<int> &st, int el){
if(st.empty()){
st.push(el);
return;
}
int top = st.top();
st.pop();
insert_at_bottom(st, el); // le daal diya bottom pe
st.push(top); // hai himmat? hai himmat sambhaalne ki?
}
void reverse(stack<int> &st){
if(st.empty()){
return;
}
int top = st.top();
st.pop();
reverse(st); // kam kar diya
insert_at_bottom(st, top); // sambhaal bsdk
}
public:
void Reverse(stack<int> &st){
reverse(st);
}
};
0.1. Count Good Numbers
- Focus on how modulo is being handled in this question, baaki toh maths hai 11th ki(PnC).
int m = (1e9+7);
class Solution {
private:
int power(long long int a, long long b){
if(a==1) return 1; // agar base hi 1 hai toh execute hi mat karo aage
if(b==0){
return 1; // kisi ki power zero toh 1
}
int temp = power(a, b/2)%m;
if(b%2==0){
return (1LL*temp*temp)%m;
}
return (1LL*a*temp*temp)%m;
}
public:
int countGoodNumbers(long long n) {
long long int even = ceil(n/2.0);
long long int odd = n/2;
return ((1LL * power(5, even))%m * (1LL * power(4, odd))%m)%m;
}
};
- Since there are multiple/variable(depending on the size of vector) recursive calls hence we need to use a
for loop
and put the recursion inside it.
class Solution {
private:
void fn(string &s, int i, string &ds, vector<string>&ans, unordered_map<int, vector<char>> &m){
if(i>=s.size()){
ans.push_back(ds);
return;
}
int digit = s[i] - '0';
for(int j=0; j<m[digit].size(); j++){
ds.push_back(m[digit][j]);
fn(s, i+1, ds, ans, m);
ds.pop_back();
}
}
public:
vector<string> letterCombinations(string &s) {
if(s.size()==0) return {};
unordered_map<int, vector<char>> m;
m[2] = {'a', 'b', 'c'};
m[3] = {'d', 'e', 'f'};
m[4] = {'g', 'h', 'i'};
m[5] = {'j', 'k', 'l'};
m[6] = {'m', 'n', 'o'};
m[7] = {'p', 'q', 'r', 's'};
m[8] = {'t', 'u', 'v'};
m[9] = {'w', 'x', 'y', 'z'};
vector<string> ans;
string ds;
fn(s, 0, ds, ans, m);
return ans;
}
};
-
unordered_map
has been used to reduce Time Complexity to O(1) on an average. -
Extract the digit, get the vector using the map and simply push it into the
ds
string to get the combinations and sinceds
has been passed on using reference hence backtrack to maintain recursive state.
- To win, the other has to lose in every scenario.
class Solution {
private:
bool fn(int n, int me){
if (n == 1 || n == 2 || n == 3)
return me;
if (me) {
return fn(n - 1, !me) || fn(n - 2, !me) || fn(n - 3, !me);
} else { // important
return fn(n - 1, !me) && fn(n - 2, !me) && fn(n - 3, !me);
}
}
public:
bool canWinNim(int n) {
// bool me = true;
// return fn(n, me);
return n % 4 != 0;
}
};
- Collection of nodes/vertices and/or edges.
- Generally in questions, N : Number of vertices and M: Number of edges.
- And in M lines we are given the edges between nodes(will make more sense in the graph representation section).
Example:
6 9 -> N = 6, M = 9
In rest of the M lines, we are given the configuration of nodes
1 3
1 5
3 5
3 4
3 6
3 2
2 6
4 6
5 6
- Undirected : no arrows to a node, i.e., no bi-directional edge.
- Directed : edges have arrows, and graphs can be traversed in a specific direction.
- Acyclic : graphs with no cycles.
- Cyclic : graphs with cycles.
- Graphs with no cycles. Acyclic undirected graphs are called trees as well.
- Number of edges(m) = Number of nodes(n) - 1.
- Root node: Could be any node, we consider rest of the tree hanging from it.
- Leaf node: Nodes without any children.
- Depth of a node: Distance of a node from the root node.
- Height of a node: Max number of edges starting from any leaf node to that node.
- LCA: The first common node of 2 or more nodes is called lowest common ancestor.
- For undirected graph:
- If from one node, every other node in that component can be traversed, then it's connected component.
- Sometimes, multiple connected components together are called a forest, its a graph too but with multiple connected components.
- Number of connected Components : The nummber of time DFS runs.
- For directed graph: Could be cyclic or acyclic depending on the direction
- We define strongly connected components here, instead of just connected components.
- Strongly connected component : It should be possible to reach at every node from the starting node.
-
Input:
6 9 1 3 1 5 3 5 3 4 3 6 3 2 2 6 4 6 5 6
-
Adjacency Matrix
-
Adjacency List → mostly used because of less space complexity
- Adjacency Matrix
- Make a matrix of n*n(n = number of vertices, m = number of edges).
- If 1-based graph then make (n+1)*(n+1).
- a[i][j] = 1 && a[j][i] = 1, if i and j are connected, else it's 0 → for undirected graph.
- a[i][j] = 1, if i and j are connected for directed ones(depending on the direction)
- if edges have weights, a[i][j] = w → w is the weight.
- Can get and edge and weight between edges in O(1)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// adjacency matrix -> too much space complexity(O(N^2))
vector<vector<int>>graph(n + 1, vector<int>(n + 1, 0)); // initialized a 2D vector
for (int i = 0; i < m; i++) {
int v1, v2;
cin >> v1 >> v2;
graph[v1][v2] = 1;
graph[v2][v1] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cout << graph[i][j] << " ";
}
cout << "\n";
}
return 0;
}
-
Output of adjacency matrix:
0 0 1 0 1 0 0 0 1 0 0 1 1 1 0 1 1 1 0 0 1 0 0 1 1 0 1 0 0 1 0 1 1 1 1 0
- Adjacency List
- Make number of lists(arrays/vectors) = number of nodes(+1 if 1-based).
- In each list store, the nod with which it is connected.
- Space Complexity = O(V+2E) ~ O(V+E)
- To find weight and edge, we need to run a loop, in O(N).
- Unweighted graph
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// adjacency list : O(V+E)
vector<vector<int>> graph(n + 1);
for (int i = 0; i < m; i++) {
int v1, v2;
cin >> v1 >> v2;
graph[v1].push_back(v2);
graph[v2].push_back(v1);
}
// N = 10^5, M<= 10^7
for (const auto& row : graph) {
for (const auto& el : row) {
cout << el << " ";
}
cout << "\n";
}
return 0;
}
- Weighted Graph
-
Input: last elements are the weights
6 9 1 3 4 1 5 3 3 5 2 3 4 7 3 6 8 3 2 9 2 6 1 4 6 2 5 6 3
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// adjacency list : O(V+E)
vector<vector<pair<int, int>>> graph(n + 1);
for (int i = 0; i < m; i++) {
int v1, v2, wt;
cin >> v1 >> v2 >> wt;
graph[v1].push_back({ v2, wt });
graph[v2].push_back({ v1, wt });
}
// N = 10^5, M<= 10^7
for (const auto& row : graph) {
for (const auto& el : row) {
cout << "{node: " << el.first << " wt:" << el.second << "}, ";
}
cout << "\n";
}
return 0;
}
-
Output:
{node: 3 wt:4}, {node: 5 wt:3}, {node: 3 wt:9}, {node: 6 wt:1}, {node: 1 wt:4}, {node: 5 wt:2}, {node: 4 wt:7}, {node: 6 wt:8},{node: 2 wt:9}, {node: 3 wt:7}, {node: 6 wt:2}, {node: 1 wt:3}, {node: 3 wt:2}, {node: 6 wt:3}, {node: 3 wt:8}, {node: 2 wt:1}, {node: 4 wt:2}, {node: 5 wt:3},
- Helps us in traversing each and every node of a forest/graph.
- Prevents infinite loops by making the node be visited only once.
Length of visited array = Length of adjacency list = Length of Adjacency Matrix = Number of nodes(if 0-based indexed graph) or Number of nodes+1(if 1-based indexed graph)
Goes in-depth of the graph/tree and visits/explores every node.
We do come back to the node from which we started.
Even if a node has been visited by some other node already, but it would be checked out by the parent node again, i.e., suppose 3 was visited by 2 already, but if 3 is a child of 1, it will atleast be checked out by 1 and then it'll do nothing as it will be visited already.
- Uses recursion.
- Used to find the number of connected components in a graph.
- Used to find cycles in a graph.
- Time Complexity: O((Number of vertices) + 2*(Number Of Edges)) ~ O(N+M)
- Code: Do remember the four areas and their significance in the code
#include <bits/stdc++.h>
using namespace std;
void dfs(int vertex, vector<int> g[], vector<bool>& vis) {
// take action on vertex after entering the vertex
vis[vertex] = true;
cout << vertex << "\n";
for (int child : g[vertex]) {
// take action on child before entering the child node(haven't called dfs yet)
cout << "par: " << vertex << ", child: " << child << "\n";
if (vis[child]) continue;
dfs(child, g, vis);
// take action on child after exiting the child node(called dfs on child)
}
// take action on vertex before exiting the vertex
}
int main() {
int n, m;
cin >> n >> m;
vector<int> g[n + 1];
vector<bool>vis(n + 1, false);
for (int i = 0; i < m; i++) {
int v1, v2;
cin >> v1 >> v2;
g[v1].push_back(v2);
g[v2].push_back(v1);
}
for (int i = 0; i < n + 1; i++) {
if (vis[i]) continue;
dfs(i, g, vis);
}
return 0;
}
-
Output:
1 par: 1, child: 3 3 par: 3, child: 1 par: 3, child: 5 5 par: 5, child: 1 par: 5, child: 3 par: 5, child: 6 6 par: 6, child: 3 par: 6, child: 2 2 par: 2, child: 3 par: 2, child: 6 par: 6, child: 4 4 par: 4, child: 3 par: 4, child: 6 par: 6, child: 5 par: 3, child: 4 par: 3, child: 6 par: 3, child: 2 par: 1, child: 5
-
Input:
8 5 1 2 2 3 2 4 3 5 6 7
-
Output : 3
#include <bits/stdc++.h>
using namespace std;
void dfs(int vertex, vector<int> g[], vector<bool>& vis) {
// take action on vertex after entering the vertex
vis[vertex] = true;
for (int child : g[vertex]) {
// take action on child before entering the child node(haven't called dfs yet)
if (vis[child]) continue;
dfs(child, g, vis);
// take action on child after exiting the child node(called dfs on child)
}
// take action on vertex before exiting the vertex
}
int main() {
int n, m;
cin >> n >> m;
vector<int> g[n + 1];
vector<bool>vis(n + 1, false);
for (int i = 0; i < m; i++) {
int v1, v2;
cin >> v1 >> v2;
g[v1].push_back(v2);
g[v2].push_back(v1);
}
int count = 0;
for (int i = 1; i < n + 1; i++) {
if (vis[i]) continue;
count++;
dfs(i, g, vis);
}
cout << count << "\n";
return 0;
}
#include <bits/stdc++.h>
using namespace std;
void dfs(int vertex, vector<int> g[], vector<bool>& vis, vector<int>& ccc) {
// take action on vertex after entering the vertex
vis[vertex] = true;
ccc.push_back(vertex);
for (int child : g[vertex]) {
// take action on child before entering the child node(haven't called dfs yet)
if (vis[child]) continue;
dfs(child, g, vis, ccc);
// take action on child after exiting the child node(called dfs on child)
}
// take action on vertex before exiting the vertex
}
int main() {
int n, m;
cin >> n >> m;
vector<int> g[n + 1];
vector<bool>vis(n + 1, false);
vector<vector<int>> cc;
for (int i = 0; i < m; i++) {
int v1, v2;
cin >> v1 >> v2;
g[v1].push_back(v2);
g[v2].push_back(v1);
}
int count = 0;
for (int i = 1; i < n + 1; i++) {
if (vis[i]) continue;
vector<int> ccc; // initialize a new current connected component vector
count++;
dfs(i, g, vis, ccc);
cc.push_back(ccc); // push it back after the dfs completes for each connected component
}
for (auto& row : cc) {
for (auto& el : row) {
cout << el << " ";
}
cout << "\n";
}
return 0;
}
- Output: 1 2 3 5 4 6 7 8
- If there's a cycle, we reach a node which is already visited but it is not the parent node from which we came from.
// Cycle detection
/*
If there's a cycle, we reach a node which is already visited but it is not the
parent node from which we came from.
*/
#include <bits/stdc++.h>
using namespace std;
bool dfs(int v, int par, vector<int>g[], vector<bool>& vis) {
vis[v] = true;
bool does_loop_exist = false;
for (int child : g[v]) {
if (vis[child] && child == par) continue;
if (vis[child]) {
return true;
};
does_loop_exist |= dfs(child, v, g, vis);
}
return does_loop_exist;
}
int main() {
int n, m;
cin >> n >> m;
vector<int>g[n + 1];
for (int i = 0; i < m; i++) {
int v1, v2;
cin >> v1 >> v2;
g[v1].push_back(v2);
g[v2].push_back(v1);
}
vector<bool>vis(n + 1, false);
for (int i = 0; i < vis.size(); i++) {
if (vis[i]) continue;
if (dfs(i, 0, g, vis)) {
cout << "true" << "\n";
return 0;
};
}
cout << "false" << "\n";
return 0;
}
Graph Matrix Problems: Flood Fill Problem
- Assume all 1's to be the nodes of a graph and that way we'll have components.
- DFS needs to be done in either 4 directions or 8(including diagonals).
#include <bits/stdc++.h>
using namespace std;
void dfs(int i, int j, int initialColor, int newColor, vector<vector<int>>& image) {
int n = image.size();
int m = image[0].size();
if (i < 0 || j < 0) return;
if (i >= n || j >= m) return;
if (image[i][j] != initialColor) return;
image[i][j] = newColor;
dfs(i - 1, j, initialColor, newColor, image);
dfs(i + 1, j, initialColor, newColor, image);
dfs(i, j - 1, initialColor, newColor, image);
dfs(i, j + 1, initialColor, newColor, image);
}
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
int initialColor = image[sr][sc];
if (initialColor != newColor)
dfs(sr, sc, initialColor, newColor, image);
return image;
}
int main() {
// Flood fill
return 0;
}
#include <bits/stdc++.h>
using namespace std;
class Solution {
private:
void dfs(int i,int j, vector<vector<char>>&g, vector<vector<bool>>&vis){
if(i<0 || j<0 || i>= g.size() || j>= g[0].size()|| g[i][j]=='0'||vis[i][j]) return;
vis[i][j] = true;
dfs(i+1, j, g, vis);
dfs(i-1, j, g, vis);
dfs(i, j+1, g, vis);
dfs(i, j-1, g, vis);
}
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<bool>>vis(m, vector<bool>(n, false));
int count = 0;
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(!vis[i][j] && grid[i][j]=='1'){
count++;
dfs(i, j, grid, vis);
}
}
}
return count;
}
};
int main(){
return 0;
}
#include <bits/stdc++.h>
using namespace std;
void dfs(int vertex, int par, vector<vector<int>>&g){
for(int child: g[vertex]){
if(child==par) continue;
dfs(child, par, g);
}
}
int main(){
int n;
cin >> n;
vector<vector<int>>graph(n + 1);
vector<int> depth(n + 1, 0);
vector<int> height(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
graph[x].push_back(y);
graph[y].push_back(x);
}
dfs(1, 0, graph);
}
- Height is calculated when coming up in the tree and depth is calculated when going down the tree.
#include <bits/stdc++.h>
using namespace std;
void dfs(int v, int par, vector<vector<int>>& g, vector<int>& depth, vector<int>& height) {
// going down into the vertex
for (int child : g[v]) {
// going down into the child
if (child == par) continue;
depth[child] = depth[v] + 1;
dfs(child, v, g, depth, height);
height[v] = max(height[child] + 1, height[v]);
// coming up from the child
}
// coming up from the vertex
}
int main() {
int n;
cin >> n;
vector<vector<int>>graph(n + 1);
vector<int> depth(n + 1, 0);
vector<int> height(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
graph[x].push_back(y);
graph[y].push_back(x);
}
dfs(1, 0, graph, depth, height);
for (int i = 1; i <= n; i++) {
cout << depth[i] << " " << height[i] << "\n";
}
return 0;
}
- Precomputation happens when you come back up from the child because you need the children's value first.
- Sum of the subtree and even number count
#include <bits/stdc++.h>
using namespace std;
void dfs(int v, int par, vector<vector<int>>& g, vector<int>& subtree_sum, vector<int>& even_ct) {
// take action while going down the vertex
subtree_sum[v] += v;
if (v % 2 == 0) even_ct[v]++;
for (int child : g[v]) {
if (child == par) continue;
// take action while going down the child
dfs(child, v, g, subtree_sum, even_ct);
subtree_sum[v] += subtree_sum[child];
even_ct[v] += even_ct[child];
// take action while coming up from the child
}
// take action while coming up from the vertex
}
int main() {
int n;
cin >> n;
vector<vector<int>>g(n + 1);
for (int i = 1; i <= n - 1; i++) {
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
vector<int> subtree_sum(n + 1);
vector<int> even_ct(n + 1);
dfs(1, 0, g, subtree_sum, even_ct);
for (int i = 1; i <= n; i++) {
cout << subtree_sum[i] << " " << even_ct[i] << "\n";
}
return 0;
}
- Find the max depth by traversing the tree through any node as root node.
- Then the node which gives the max depth,i.e., the max_depth_node use it as a root node and find the max depth again.
- These two nodes(max_depth_node and max_depth_node_1) are the two ends of the diameter and the newly calculated max_depth is the diameter.
#include <bits/stdc++.h>
using namespace std;
void dfs(int v, int p, vector<int>& d, vector<vector<int>>& g) {
for (int c : g[v]) {
if (c == p) continue;
d[c] = d[v] + 1;
dfs(c, v, d, g);
}
}
void reset(vector<int>& d) {
for (int i = 0; i < d.size(); i++) {
d[i] = 0;
}
}
int main() {
int n;
cin >> n;
vector<vector<int>>g(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y; cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
vector<int>d(n + 1, 0);
dfs(1, -1, d, g);
int max_depth_node = -1;
int max_depth = -1;
for (int i = 1; i <= n; i++) {
if (max_depth <= d[i]) {
max_depth = d[i];
max_depth_node = i;
}
}
cout << max_depth_node << " " << max_depth << "\n";
reset(d);
dfs(max_depth_node, 0, d, g);
int max_depth_node_1 = -1;
int max_depth_1 = -1;
for (int i = 1; i <= n; i++) {
if (max_depth_1 <= d[i]) {
max_depth_1 = d[i];
max_depth_node_1 = i;
}
}
cout << max_depth_node_1 << " " << max_depth_1 << "\n";
int diameter = max_depth_1;
cout << diameter;
// max_depth_node and max_depth_node_1 are the two ends of the diameter.
return 0;
}
- Store the parents in a parents array and then using that parents array climb back to the root node and store the path of nodes from root node and then simply traverse through both the path array and wherever the equality the node just before it is the LCA.
#include <bits/stdc++.h>
using namespace std;
void dfs(int v, int p, vector<vector<int>>& g, vector<int>& par) {
par[v] = p;
for (int c : g[v]) {
if (c == p) continue;
dfs(c, v, g, par);
}
}
vector<int> path(int v, vector<int>& par) {
vector<int> p;
while (true) {
if (v == -1) break;
p.push_back(v);
v = par[v];
}
reverse(p.begin(), p.end());
return p;
}
int main() {
int n;
cin >> n;
vector<vector<int>>g(n + 1);
vector<int> par(n + 1, -1);
for (int i = 0; i < n - 1; i++) {
int x, y; cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
// take any 2 nodes for their LCA, suppose 2 and 12 -> their LCA is 2
dfs(1, -1, g, par);
vector<int>p1 = path(12, par);
vector<int>p2 = path(2, par);
for (auto& it : p1) {
cout << it << " ";
}
cout << "\n";
for (auto& it : p2) {
cout << it << " ";
}
cout << "\n";
int LCA = -1;
for (int i = 0; i < n-1; i++) {
if (p1[i] != p2[i]) break;
LCA = p1[i];
}
cout << LCA;
return 0;
}
- Given a tree with
n
nodes, where each node is labeled with a unique integer from 1 ton
, you need to compute the maximum possible product formed by deleting a single edge in the tree. When an edge is deleted, the tree is divided into two separate subtrees. The product is calculated as the product of the sums of the node values in each of these two subtrees.
#include <bits/stdc++.h>
using namespace std;
void dfs(int v, int par, vector<vector<int>>& g, vector<int>& subtree_sum) {
subtree_sum[v] = v;
for (int c : g[v]) {
if (c == par) continue;
dfs(c, v, g, subtree_sum);
subtree_sum[v] += subtree_sum[c];
}
}
int main() {
int n;
cin >> n;
vector<vector<int>>g(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y; cin >> x >> y;
g[y].push_back(x);
g[x].push_back(y);
}
vector<int>subtree_sum(n + 1, 0);
dfs(1, -1, g, subtree_sum);
// delete edge: Delete the top edge of the node
long long ans = 1;
for (int i = 2; i <= n; i++) {
int part_1 = subtree_sum[i];
int part_2 = subtree_sum[1] - subtree_sum[i];
ans = max(ans, part_1 * 1LL * part_2);
}
cout << ans << "\n";
return 0;
}
-
Given a tree with
n
nodes, where each node is labeled with a unique integer from 1 ton
, your task is to:- Count how many prime numbers exist in the subtree of each node, including the node itself.
- Find the maximum possible product formed by deleting a single edge in the tree, where the product is computed as the number of prime numbers in the two resulting subtrees after the deletion.
- Input:
- The first line contains a single integer n representing the number of nodes in the tree.
- The next n-1 lines each contain two integers x and y, representing an undirected edge between node x and node y.
- Output:
- For each node from 1 to n, print the count of prime numbers in its subtree.
- Output the maximum product that can be obtained by deleting exactly one edge, where the product is the multiplication of prime counts in the two resulting subtrees.
#include <bits/stdc++.h>
using namespace std;
void dfs(int v, int par, vector<vector<int>>& g, vector<int>& subtree_prime_count, vector<bool>& isPrime) {
if (isPrime[v])
subtree_prime_count[v]++;
for (int c : g[v]) {
if (c == par) continue;
dfs(c, v, g, subtree_prime_count, isPrime);
subtree_prime_count[v] += subtree_prime_count[c];
}
}
int main() {
int n;
cin >> n;
vector<vector<int>>g(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y; cin >> x >> y;
g[y].push_back(x);
g[x].push_back(y);
}
vector<int>subtree_prime_count(n + 1, 0);
vector<bool>isPrime(n + 1, true);
isPrime[1] = isPrime[0] = 0;
for (int i = 2; i < n + 1; i++) {
if (isPrime[i]) {
for (int j = 2 * i; j < n + 1; j += i) {
isPrime[j] = 0;
}
}
}
dfs(1, -1, g, subtree_prime_count, isPrime);
for (int i = 1; i <= n; i++) {
cout << subtree_prime_count[i] << "\n";
}
// delete edge: Delete the top edge of the node
long long ans = 1;
for (int i = 2; i <= n; i++) {
int part_1 = subtree_prime_count[i];
int part_2 = subtree_prime_count[1] - subtree_prime_count[i];
ans = max(ans, part_1 * 1LL * part_2);
}
cout << "ans: " << ans << "\n";
return 0;
}
- Goes level wise.
There are two levels at a time in the queue, the current one and the next one.
Time Complexity is O(V+E).
- Each node is visited once, because of the visited array ofcourse.
In an equally weighted graph, the path given from the source node is the shortest.
- The smaller level will always be accessed by the BFS firstly then it goes to the greater ones!
#include <bits/stdc++.h>
using namespace std;
void bfs(int source, vector<vector<int>>& g, vector<int>& b, vector<bool>& vis, int n) {
queue<int>q;
q.push(1);
b.push_back(1);
vis[1] = true;
vector<int>level(n + 1, 0);
while (!q.empty()) {
int current_node = q.front();
q.pop();
b.push_back(current_node);
for (int child : g[current_node]) {
if (vis[child]) continue;
q.push(child);
vis[child] = true;
level[child] = level[current_node] + 1;
}
}
for (int i = 1; i <= n; i++) {
cout << b[i] << ", level[" << b[i] << "]: " << level[i] << "\n";
}
}
int main() {
int n;
cin >> n;
vector<vector<int>>g(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y; cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
vector<bool>vis(n + 1, 0);
vector<int>b;
bfs(1, g, b, vis, n);
return 0;
}
- Find the minimum number of moves a knight takes to reach from one square to another square of a chess board (8 × 8).
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9 + 10;
int getX(string s) {
return s[0] - 'a';
}
int getY(string s) {
return s[1] - '1';
}
int vis[8][8];
int lev[8][8];
vector<pair<int, int>> movements = {
{1,2}, {-1,2},
{2,1}, {2,-1},
{-2,-1}, {-2,1},
{1,-2}, {-1,-2}
};
bool isValid(int x, int y) {
return x >= 0 && y >= 0 && x < 8 && y < 8;
}
int bfs(string src, string dest) {
int srcX = getX(src);
int srcY = getY(src);
int destX = getX(dest);
int destY = getY(dest);
queue<pair<int, int>> q;
q.push({ srcX, srcY });
vis[srcX][srcY] = 1;
while (!q.empty()) {
pair<int, int> p = q.front();
q.pop();
int x = p.first;
int y = p.second;
for (auto movement : movements) {
int childX = movement.first + x;
int childY = movement.second + y;
if (!isValid(childX, childY)) continue;
if (vis[childX][childY]) continue;
q.push({ childX, childY });
lev[childX][childY] = lev[x][y] + 1;
vis[childX][childY] = 1;
}
if (lev[destX][destY] != INF) break;
}
return lev[destX][destY];
}
void reset() {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
lev[i][j] = INF;
vis[i][j] = 0;
}
}
}
int main() {
int t;
cin >> t;
while (t--) {
reset();
string s1, s2;
cin >> s1 >> s2;
cout << bfs(s1, s2);
}
return 0;
}
- We process the node again(two times) even if it has been processed once, i.e., went into the queue once already, that's why no visited array as we need to process the path with wt = 0(which could result in the even shorter path), that's why we used the level technique, the level technique can be used in regular BFS(here, the nodes are processed only once) too, by setting the level once and then skipping the nodes whose level isn't INF.
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9 + 10;
int bfs(vector<vector<pair<int, int>>>& g, vector<int>& lev, int n) {
deque<int> q;
q.push_back(1);
lev[1] = 0;
while (!q.empty()) {
int cur_v = q.front();
q.pop_front();
for (auto child : g[cur_v]) {
int child_v = child.first;
int wt = child.second;
if (lev[cur_v] + wt < lev[child_v]) {
lev[child_v] = lev[cur_v] + 1;
/* to process the node again even if it has been
processed already, that's why no visited array as we need to process the path with
wt = 0, that's why we used the level technique, it can be used in regular BFS too, by
setting the level once and then skipping the nodes whose level isn't INF.
*/
if (wt == 0) {
q.push_front(child_v);
} else {
q.push_back(child_v);
}
}
}
}
return lev[n] == INF ? -1 : lev[n]; // lev[n] because we need to check for the shortest path between 1 to N
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<pair<int, int>>>g(n + 1);
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
if (x == y) continue; // do avoid self loops, i.e., edge between the same element
g[x].push_back({ y,0 });
g[y].push_back({ x,1 });
}
vector<int> lev(n + 1, INF);
cout << bfs(g, lev, n) << " ";
return 0;
}
-
Multi-source BFS basically gives us the idea that if we start from multiple sources at once then which one will lead us to our destination first and its then marked visited ofcourse.
-
We basically put all the sources into the queue all at once and run the BFS.
-
Why it works?
-
When we put multiple sources into the queue and initialize them with level = 0, then all the nodes other than the sources would be reached by the ones already in the queue and will be marked as level = 1 and so on.
-
Even if a node was visitable by a source node but was at a farther distance/level it won't be visited again, because that node would already have been marked visited/the level would be set to 1 which won't allow other farther source nodes to intefere.
-
-
Examples could include the following:
- Reaching at the destination node in shortest time.
- Which node could lead us to the destination node faster.
- Finding the shortest time to reach a node when started with multiple nodes.
- Questions could be on simultaneity of a situation, i.e., the values increase/decrease/change simultaneously in a grid.
- Recursion → Memoization(for time complexity optimization) → Tabulation(for eliminating space complexity of the stack space) → Space Optimization to O(1)
- Overlapping sub-problems: we store(
memoize
) the results of sub-problems in a map/table and then simply retrieve it, when they overlap. - Memoization: Top→Down → build from the top(required solution) to the base case
- Tabulation: Bottom→up → build from base case till the required solution
- Space Optimization: Eliminating the
dp
array altogether and make it O(1).- If there is (index-1) and (index-2) involved, we can always optimize the space.
- Recursive way : Time Complexity → O(2N), Space Complexity → O(N)
#include <bits/stdc++.h>
using namespace std;
int fibo(int n) {
// recursion
if (n < 2) {
return n;
}
return fibo(n - 1) + fibo(n - 2); // recurrence relation
}
int main() {
cout << fibo(4) << "\n";
return 0;
}
- Memoization: Time Complexity → O(N), Space Complexity → O(2N) = Stack Space + Array Space
#include <bits/stdc++.h>
using namespace std;
// memoization: Time Complexity -> O(N), Space Complexity -> O(2N) = Stack Space + Array Space
int f_memo(int n, vector<int>& dp) {
if (n <= 1) {
return n;
}
if (dp[n] != -1) return dp[n];
return dp[n] = f_memo(n - 1, dp) + f_memo(n - 2, dp);
}
int main() {
int n;
cin >> n;
vector<int> dp(n + 1, -1);
cout << f_memo(n, dp) << "\n";
return 0;
}
- Tabulation: Eliminating stack space in this case
- Time Complexity → O(N), Space Complexity → O(N) = Array Space
Convert the recurrence relation from f(n-1) + f(n-2)
→dp[i-1] + dp[i-2].
#include <bits/stdc++.h>
using namespace std;
//tabulation: Time Complexity -> O(N), Space Complexity -> O(N) = Array Space
int f_tabulated(int n, vector<int>& dp) {
dp[0] = 0; dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
int main() {
int n;
cin >> n;
vector<int> dp(n + 1, -1);
cout << f_tabulated(n, dp);
return 0;
}
- Making the space complexity = O(1), by using
prev
,prev2
andcuri
pointers → Remember this pattern as it will be used or its variations would be used at many places
#include <bits/stdc++.h>
using namespace std;
// space optimization: Time Complexity -> O(N), Space Complexity -> O(1)
int f_space_optimized(int n) {
int prev2 = 0, prev = 1;
if (n < 2) return n;
for (int i = 2; i <= n; i++) {
int curi = prev2 + prev;
prev2 = prev;
prev = curi;
}
return prev;
}
int main() {
int n;
cin >> n;
cout << f_space_optimized(n) << "\n";
return 0;
}
- Count the number of ways
- Multiple ways of doing something, then minimum or maximum number of ways are asked
Recursion helps you explore all the possibilities.
- Try to represent the problem in terms of indices(even if there is no array).
- Do all possible things on that index according to the problem statement.
- Think of edge cases.
- If you need to do the following:
- Count all ways: Sum up all the things
- Min(of all things): Find Min
- Max(of all things): Find Max
- Example 1: Climbing Stairs
#include <bits/stdc++.h>
using namespace std;
// this is the recursive way(btw it looks like fibonacci)
int climb_stairs(int i) { // we'll jump down
if (i == 0) return 1;
// however, what if i=1, then we'll need to handle the edge case of i = 1
if (i == 1) return 1;
int left = climb_stairs(i - 1);
int right = climb_stairs(i - 2);
return left + right;
}
// let's memoize it
int f(int i, vector<int>& dp) {
if (i == 0) return 1;
if (i == 1) return 1;
if (dp[i] != -1) return dp[i];
int left = f(i - 1, dp);
int right = f(i - 2, dp);
return dp[i] = left + right;
}
// let's tabulate this
int t(int n, vector<int>& dp) { // size of dp vector would be n+1, for 1-based indexing in this case
if (n == 0) return 1;
if (n == 1) return 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
//let's do space optimization
int s(int n) {
if (n == 0) return 1;
if (n == 1) return 1;
int prev = 1, prev2 = 1;
for (int i = 0; i <= n; i++) {
int curi = prev2 + prev;
prev2 = prev;
prev = curi;
}
return prev;
}
int main() {
// Represent the problem in terms of indices.
// We'll consider the stairs to be indices from 0 to n.
// Then either we can jump 1 step or 2 steps -> the things we can do with the index.
return 0;
}
- Example 2: Frog Jump
- If there is (index-1) and (index-2) involved, we can always optimize the space.
#include <bits/stdc++.h>
using namespace std;
int min_energy_recursive(int ind, vector<int>& heights) {
if (ind == 0) return 0;
if (ind == 1) return abs(heights[1] - heights[0]);
int left = min_energy_recursive(ind - 1, heights) + abs(heights[ind] - heights[ind - 1]);
int right = min_energy_recursive(ind - 2, heights) + abs(heights[ind] - heights[ind - 2]);
return (left, right);
}
int min_energy_dp(int ind, vector<int>& dp, vector<int>& heights) {
if (ind == 0) return 0;
if (ind == 1) return abs(heights[1] - heights[0]);
if (dp[ind] != -1) return dp[ind];
int left = min_energy_dp(ind - 1, dp, heights) + abs(heights[ind] - heights[ind - 1]);
int right = min_energy_dp(ind - 2, dp, heights) + abs(heights[ind] - heights[ind - 2]);
return dp[ind] = min(left, right);
}
int min_energy_tabulated(int ind, vector<int>& dp, vector<int>& heights) {
if (ind == 0) return 0;
if (ind == 1) return abs(heights[1] - heights[0]);
dp[0] = 0; dp[1] = abs(heights[1] - heights[0]);
for (int i = 2; i <= ind; i++) {
dp[i] = min(dp[i - 1] + abs(heights[i] - heights[i - 1]), dp[i - 2] + abs(heights[i] - heights[i - 2]));
}
return dp[ind];
}
int min_energy_space_optimized(int ind, vector<int>& heights) {
if (ind == 0) return 0;
if (ind == 1) return abs(heights[1] - heights[0]);
int prev = abs(heights[1] - heights[0]), prev2 = 0;
for (int i = 2; i <= ind; i++) {
int curi = min(prev + abs(heights[i] - heights[i - 1]), prev2 + abs(heights[i] - heights[i - 2]));
prev2 = prev;
prev = curi;
}
return prev;
}
int main() {
// Express the question in terms of an index
// Then do everything with the index as per the question
// Think of the edge cases, like ind == 1, in this case.
// Then return the minimum of the things you did on the index
return 0;
}
- Example 3: Frog Jump with K steps
- It demonstrates how 2 loops are required in the tabulated format.
#include <bits/stdc++.h>
using namespace std;
int jump_with_k_steps(int n, int k, vector<int>& arr) {
if (n == 0) return 0;
int min_steps = INT_MAX;
for (int i = 1; i <= k; i++) {
if (n - i >= 0) {
int jump = jump_with_k_steps(n - i, k, arr);
min_steps = (min_steps, jump);
}
}
return min_steps;
}
int jump_with_k_steps_memoized(int i, int k, vector<int>& arr, vector<int>& dp) {
if (i == 0) return 0;
if (dp[i] != -1) return dp[i];
int min_steps = INT_MAX;
for (int j = 1; j <= k; j++) {
if (i - j >= 0) {
int jump = jump_with_k_steps_memoized(i - j, k, arr, dp) + abs(arr[i - j] - arr[i]);
min_steps = (min_steps, jump);
}
}
return dp[i] = min_steps;
}
int jump_with_k_steps_tabulated(int n, int k, vector<int>& arr, vector<int>& dp) {
if (n == 0) return 0;
dp[0] = 0;
for (int i = 1; i <= n; i++) {
int min_steps = INT_MAX;
for (int j = 1; j <= k; j++) {
if (i - j >= 0) {
int jump = dp[i - j] + abs(arr[i - j] - arr[i]);
min_steps = min(jump, min_steps);
}
}
dp[i] = min_steps;
}
return dp[n];
}
int main() {
int n, k;
cin >> n >> k;
vector<int> arr(n);
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
vector<int> dp(n + 1, -1);
cout << jump_with_k_steps_tabulated(n - 1, k, arr, dp);
return 0;
}