Wednesday, October 28, 2026

Dijkstra's Algorithm in C++ with a Priority Queue

Breadth-first search finds shortest paths when every edge has the same length. As soon as edges have different weights, such as road lengths or travel times, BFS gives wrong answers. Dijkstra's algorithm is the standard fix for graphs whose edge weights are non-negative.

The problem

There are n cities and m one-way flights, each with a cost. Find the cheapest cost from city 1 to every city. This is CSES 1671 – Shortest Routes I, with n up to 105 and m up to 2 × 105.

The idea

Keep a tentative distance for every node, starting at 0 for the source and infinity for the rest. Then repeat:

  1. Take the unfinished node u with the smallest tentative distance. That distance is now final.
  2. For every edge u → v with weight w, check whether going through u is cheaper: if dist[u] + w < dist[v], update dist[v]. This is called relaxing the edge.

Why the smallest node is final

Suppose u has the smallest tentative distance d. Any other route to u would have to leave the set of finished nodes through some unfinished node, whose distance is already at least d. Adding more edges can only make that route longer, because weights are non-negative. So no route can beat d.

That argument is exactly where negative edges break things: a negative edge could make a longer-looking path cheaper later. With negative weights, use Bellman–Ford instead.

Finding the minimum quickly

Scanning all nodes for the minimum each time is O(n2). A min-heap (priority_queue with greater<>) gives the smallest entry in O(log n). Whenever we improve a distance, we push the new (distance, node) pair.

A node can end up in the heap several times with older, larger distances. Instead of deleting those, we simply skip any entry whose distance does not match the current dist[u]. These are called stale entries.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;
    vector<vector<pair<int, long long>>> adj(n + 1);
    for (int i = 0; i < m; i++) {
        int a, b;
        long long w;
        cin >> a >> b >> w;
        adj[a].push_back({b, w});   // directed edge a -> b with weight w
    }

    const long long INF = LLONG_MAX;
    vector<long long> dist(n + 1, INF);
    // min-heap of (distance, node)
    priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> pq;

    dist[1] = 0;
    pq.push({0, 1});
    while (!pq.empty()) {
        auto [d, u] = pq.top();
        pq.pop();
        if (d != dist[u]) continue;          // stale entry, skip it
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }

    for (int i = 1; i <= n; i++)
        cout << dist[i] << (i == n ? '\n' : ' ');
    return 0;
}

Example

3 4
1 2 6
1 3 2
3 2 3
1 3 4

The direct flight 1 → 2 costs 6, but 1 → 3 → 2 costs 2 + 3 = 5. Output: 0 5 2.

Step by step: pop city 1 (distance 0) and relax its edges, so city 2 gets 6 and city 3 gets 2. Pop city 3 (distance 2) and relax 3 → 2, improving city 2 to 5. Pop city 2 (distance 5). The old entry (6, city 2) is popped later and skipped as stale.

Complexity

Each edge can push at most one entry, so the heap holds O(m) entries. Total time is O((n + m) log m), and memory is O(n + m).

Common mistakes

  • Using a max-heap. C++'s priority_queue returns the largest element by default. Use greater<>, or push negative distances.
  • Forgetting the stale check. Without if (d != dist[u]) continue; the answer is still correct, but on some graphs the algorithm becomes much slower.
  • Using int distances. A path of 105 edges with weight 109 each sums to 1014. Use long long.
  • Undirected graphs. If roads go both ways, add each edge in both directions.

Practice problems

If BFS is still new to you, start with the BFS deep dive. Dijkstra is the same idea with a priority queue in place of a plain queue.

Disjoint Set Union (Union-Find) Explained with C++ Code

Disjoint Set Union (DSU), also called Union-Find, answers one question extremely quickly: are these two things in the same group? It also lets you merge groups as you go. That makes it perfect for problems where connections are added over time, such as roads being built between cities.

The problem

There are n cities and no roads. Roads are built one at a time. After each new road, print how many separate connected groups of cities there are, and the size of the largest group. This is CSES 1676 – Road Construction, with up to 105 cities and 2 × 105 roads.

Running a BFS or DFS after every road would cost O(n + m) each time, which is far too slow. DSU handles each road in almost constant time.

How DSU represents groups

Each group is a tree. Every element points to a parent, and the root of the tree points to itself. The root is the “leader” that represents the whole group. Two elements are in the same group exactly when they have the same leader.

  • find(x): follow parent pointers from x until you reach the root.
  • unite(a, b): find both leaders. If they differ, make one leader point to the other. The two groups are now one.

Two optimisations that make it fast

Without care, the trees can become long chains, and find becomes O(n). Two tricks prevent that:

  1. Union by size. When merging, attach the smaller tree under the larger one. A node's depth only increases when its group at least doubles in size, so no tree gets deeper than log2 n.
  2. Path compression. During find, make nodes point closer to the root. The code below uses “path halving”, which points each visited node at its grandparent. Later lookups on the same path become almost instant.

With both, each operation takes amortised O(α(n)) time, where α is the inverse Ackermann function. It grows so slowly that it is at most 4 for any input that fits in the universe, so in practice it is constant.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> parent, sz;
    DSU(int n) : parent(n + 1), sz(n + 1, 1) {
        iota(parent.begin(), parent.end(), 0);   // everyone is their own leader
    }
    int find(int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];       // path halving
            x = parent[x];
        }
        return x;
    }
    // Returns true if a and b were in different sets.
    bool unite(int a, int b) {
        a = find(a);
        b = find(b);
        if (a == b) return false;
        if (sz[a] < sz[b]) swap(a, b);           // union by size
        parent[b] = a;
        sz[a] += sz[b];
        return true;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;
    DSU dsu(n);
    int components = n, largest = 1;

    while (m--) {
        int a, b;
        cin >> a >> b;
        if (dsu.unite(a, b)) {
            components--;
            largest = max(largest, dsu.sz[dsu.find(a)]);
        }
        cout << components << ' ' << largest << '\n';
    }
    return 0;
}

Each successful unite merges two groups, so the number of components drops by one. The size of the new group is stored at its leader, which makes the “largest group” easy to track.

Example

5 3
1 2
1 3
4 5

Output:

4 2
3 3
2 3

After road 1–2 there are 4 groups and the biggest has 2 cities. After 1–3, that group grows to 3. After 4–5, there are 2 groups: {1, 2, 3} and {4, 5}.

Complexity

O((n + m) · α(n)) time and O(n) memory.

Where DSU shows up

  • Kruskal's minimum spanning tree. Sort edges by weight and add each one if DSU says its endpoints are not yet connected.
  • Cycle detection in an undirected graph: an edge whose endpoints already share a leader closes a cycle.
  • Connectivity while edges are added: answer “are u and v connected?” at any moment as the graph grows.

DSU cannot easily remove edges. If a problem deletes connections, a common trick is to process everything in reverse, turning deletions into additions.

Practice problems

Bitmasks in Competitive Programming: Trying Every Subset

When a problem has n ≤ 20 and asks you to choose some of the items, the intended solution is often to try every possible choice. There are 2n of them, about a million for n = 20, which is fast enough. Bitmasks are the cleanest way to list them all.

An integer as a set

Think of the bits of an integer as yes/no switches. With n items, bit i of a number says whether item i is in the set. For 4 items:

  • 0000 (0) is the empty set.
  • 0101 (5) is the set {item 0, item 2}.
  • 1111 (15) is all four items.

Every integer from 0 to 2n − 1 is a different subset, and every subset appears exactly once. So a single for loop over those integers visits all subsets.

The bit operations you need

GoalExpression
Is item i in the set?mask & (1 << i)
Add item imask | (1 << i)
Remove item imask & ~(1 << i)
Number of items in the set__builtin_popcount(mask)
Number of subsets of n items1 << n

Example problem: dividing apples

There are n ≤ 20 apples with weights p1, …, pn. Split them into two groups so that the difference between the group weights is as small as possible. This is CSES 1623 – Apple Division.

Each mask chooses which apples go into group A. Group B gets the rest, so its weight is the total minus A. The difference is |total − 2A|. Try every mask and keep the smallest difference.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<long long> p(n);
    long long total = 0;
    for (auto &x : p) { cin >> x; total += x; }

    long long best = LLONG_MAX;
    // Each mask from 0 to 2^n - 1 describes one way to choose group A:
    // bit i set means apple i goes to group A.
    for (int mask = 0; mask < (1 << n); mask++) {
        long long a = 0;
        for (int i = 0; i < n; i++)
            if (mask & (1 << i)) a += p[i];
        best = min(best, llabs(total - 2 * a));   // |A - B| where B = total - A
    }
    cout << best << '\n';
    return 0;
}

Example

Weights 3 2 7 4 1, total 17. Putting {3, 4, 1} in one group (8) and {2, 7} in the other (9) gives a difference of 1. A difference of 0 is impossible because the total is odd, so 1 is optimal.

Complexity

2n masks, and O(n) work for each, gives O(2n · n). For n = 20 that is about 2 × 107 operations. For n = 30 it would be 3 × 1010, which is why this technique is tied to small limits. My post on reading constraints has more on matching limits to techniques.

Common mistakes

  • Writing 1 << n when n can be 31 or more. That overflows a 32-bit int; use 1LL << n.
  • Operator precedence. mask & 1 << i == 0 does not mean what it looks like, because == binds tighter than &. Add parentheses: (mask & (1 << i)) == 0.
  • Recomputing sums from scratch for each mask when n is near the limit. It works here, but for tighter problems you can build the sum of a mask from a smaller mask in O(1).

Where to go next

Bitmasks are also used as DP states. In “bitmask DP”, dp[mask] stores the best answer for a particular subset of items already used. That is how the travelling salesman problem is solved for n ≤ 20 in O(2n · n2).

Practice problems

Dynamic Programming for Beginners: From Recursion to a DP Table

Dynamic programming (DP) has a reputation for being the hardest topic for beginners. A lot of that comes from the name, which says nothing about what it does. The real idea is simple: if a problem breaks into smaller versions of itself, and those smaller versions repeat, solve each one once and store the answer.

This post builds up DP with two classic problems, the frog and coin change, and gives a checklist you can reuse.

Problem 1: the frog

There are n stones in a row with heights h1, …, hn. A frog starts on stone 1 and wants to reach stone n. From stone i it can jump to i + 1 or i + 2, and a jump costs the absolute difference in height. What is the minimum total cost? This is AtCoder DP Contest A – Frog 1.

Step 1: define the state

Let dp[i] = the minimum cost to reach stone i. Choosing what dp[i] means is the most important decision in any DP. A good state is small and captures everything the future depends on. Here, once the frog is on stone i, it does not matter how it got there.

Step 2: write the transition

The frog reaches stone i either from stone i − 1 or from stone i − 2. So:

dp[i] = min( dp[i-1] + |h[i] - h[i-1]|,
             dp[i-2] + |h[i] - h[i-2]| )

Step 3: base case and order

The cost to reach the first stone is 0, since the frog starts there. Every dp[i] only uses smaller indices, so we fill the table from left to right. (The code below numbers stones from 0, as C++ arrays do.)

Why not plain recursion?

You could write the formula as a recursive function. But computing dp[i] calls dp[i − 1] and dp[i − 2], each of which calls two more, and the number of calls grows like the Fibonacci numbers, which is exponential. The same values get recomputed again and again. Storing each answer the first time (either with memoization or with a table like below) makes it linear.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> h(n);
    for (auto &x : h) cin >> x;

    // dp[i] = minimum cost to reach stone i
    vector<long long> dp(n, LLONG_MAX);
    dp[0] = 0;
    for (int i = 1; i < n; i++) {
        dp[i] = dp[i - 1] + abs(h[i] - h[i - 1]);
        if (i >= 2)
            dp[i] = min(dp[i], dp[i - 2] + abs(h[i] - h[i - 2]));
    }
    cout << dp[n - 1] << '\n';
    return 0;
}

For heights 10 30 40 20, the best path is 1 → 2 → 4 with cost 20 + 10 = 30. The table is dp = [0, 20, 30, 30].

Complexity: O(n) time and memory.

Problem 2: fewest coins

You have coin values c1, …, cn, with unlimited coins of each. What is the fewest number of coins that add up to exactly x? Print −1 if it is impossible. This is CSES 1634 – Minimizing Coins.

Greedy (always take the largest coin) fails here. With coins 1, 5 and 7 and x = 10, greedy takes 7 + 1 + 1 + 1 = 4 coins, but 5 + 5 uses only 2.

  • State: dp[s] = fewest coins that sum to exactly s.
  • Transition: the last coin used is some c, so dp[s] = min over coins of dp[s − c] + 1.
  • Base case: dp[0] = 0, and every other value starts as “impossible”.
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, x;
    cin >> n >> x;
    vector<int> c(n);
    for (auto &v : c) cin >> v;

    const int INF = 1e9;
    // dp[s] = fewest coins that add up to exactly s
    vector<int> dp(x + 1, INF);
    dp[0] = 0;
    for (int s = 1; s <= x; s++)
        for (int coin : c)
            if (coin <= s && dp[s - coin] != INF)
                dp[s] = min(dp[s], dp[s - coin] + 1);

    cout << (dp[x] == INF ? -1 : dp[x]) << '\n';
    return 0;
}

Complexity: O(n · x) time and O(x) memory.

A checklist for any DP problem

  1. State: what does dp[…] mean, in one sentence?
  2. Transition: what was the last decision, and how does it connect to smaller states?
  3. Base cases: which states can you answer directly?
  4. Order: in which order must states be filled so that dependencies are ready?
  5. Answer: which state, or combination of states, is the final answer?

If you can answer all five in plain words before coding, the code is usually short.

Practice problems

Greedy Interval Scheduling: Why You Sort by End Time

Greedy algorithms make the choice that looks best right now and never go back. Sometimes that works perfectly and sometimes it fails badly. Interval scheduling is the classic case where it works, and it is a great example for learning how to prove that a greedy choice is correct rather than just hoping.

The problem

There are n movies at a festival. Movie i starts at time si and ends at time ei. You can only watch one movie at a time, and you must watch each one from start to finish. A movie may start at the exact moment another one ends. What is the largest number of movies you can watch? This is CSES 1629 – Movie Festival, with n up to 2 × 105.

Greedy ideas that do not work

  • Earliest start first. A very long movie that starts first would block everything else.
  • Shortest movie first. A short movie can sit right across the boundary of two longer movies that would otherwise both fit, so choosing it costs you two.

Trying to break your own greedy idea with small examples like these is the most useful habit for greedy problems.

The rule that works: earliest end first

Sort movies by end time. Go through them in that order, and watch a movie whenever it starts after (or exactly when) the last chosen movie ended.

The intuition is that finishing early leaves as much of the day free as possible for everything that comes after.

Why it is correct (exchange argument)

Take any optimal schedule and look at its first movie. Our greedy picks the movie with the earliest end time overall. That greedy movie ends no later than the optimal schedule's first movie, so we can swap it in. The rest of the optimal schedule still fits, because everything in it starts after the old first movie ended, which is no earlier than when the greedy movie ends.

So there is an optimal schedule that begins with the greedy choice. Remove that movie and everything that overlaps it, and the same argument applies to the rest. By repeating this, the greedy schedule is as long as an optimal one.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<pair<int, int>> movies(n);   // stored as (end, start)
    for (auto &[e, s] : movies) cin >> s >> e;

    sort(movies.begin(), movies.end());  // earliest end time first

    int watched = 0;
    int free_at = 0;                     // when we are next free
    for (auto [e, s] : movies) {
        if (s >= free_at) {
            watched++;
            free_at = e;
        }
    }
    cout << watched << '\n';
    return 0;
}

Storing each pair as (end, start) means the default sort orders by end time, with no custom comparator needed.

Example

3
3 5
4 9
5 8

Sorted by end time: (3, 5), (5, 8), (4, 9). We watch 3–5, then 5–8 (it starts right as the first ends), and skip 4–9 because it starts before 8. Answer: 2.

Complexity

Sorting is O(n log n) and the scan is O(n).

Be careful with the boundary

Whether a movie may start exactly when the previous one ends decides between >= and >. Read the statement for that detail every time. Getting it wrong passes the samples surprisingly often and then fails on hidden tests.

Related problems

  • Minimum number of points to hit every interval. Same sorting, and the same proof idea: place a point at the end of the earliest-ending interval not yet hit.
  • Minimum rooms so that every meeting fits. Different greedy: sort by start time and track when rooms become free, usually with a priority queue.

Practice problems

If you are newer to proofs like this, Domino Piling (50A) uses the same “upper bound plus construction” style of reasoning in a simpler setting.

GCD and LCM in C++: Euclid's Algorithm Explained

The greatest common divisor (GCD) is one of the oldest algorithms in mathematics, and it still shows up constantly in contest problems: simplifying fractions, synchronising cycles, and in clever tricks that turn a hard-looking problem into one line. This post covers how Euclid's algorithm works, why it is fast, and the LCM overflow trap.

Definitions

gcd(a, b) is the largest number that divides both a and b. lcm(a, b) is the smallest positive number that both a and b divide. For example gcd(12, 18) = 6 and lcm(12, 18) = 36. They are linked by a simple identity:

gcd(a, b) × lcm(a, b) = a × b

Euclid's algorithm

The key fact is gcd(a, b) = gcd(b, a mod b). Any number that divides both a and b also divides a − kb for any k, and a mod b is exactly such a number. The reverse also holds, so both pairs have the same common divisors and therefore the same greatest one.

Keep replacing the pair until the second number is 0. Then gcd(g, 0) = g.

Example for gcd(252, 105):

gcd(252, 105) -> 252 mod 105 = 42
gcd(105, 42)  -> 105 mod 42  = 21
gcd(42, 21)   -> 42 mod 21   = 0
gcd(21, 0)    = 21

Why it is fast

After two steps, the larger number always drops to less than half of what it was. So the number of steps is O(log min(a, b)), at most about 90 steps for numbers up to 1018. The slowest inputs are consecutive Fibonacci numbers, which is a fun fact worth remembering.

Code (C++)

This program reads n numbers and prints the GCD and LCM of all of them.

#include <bits/stdc++.h>
using namespace std;

long long gcd_ll(long long a, long long b) {
    while (b != 0) {
        long long r = a % b;
        a = b;
        b = r;
    }
    return a;
}

int main() {
    int n;
    cin >> n;
    vector<long long> a(n);
    for (auto &x : a) cin >> x;

    long long g = 0, l = 1;
    for (long long x : a) {
        g = gcd_ll(g, x);            // gcd(0, x) = x, so 0 is a safe start
        l = l / gcd_ll(l, x) * x;    // divide first to keep numbers small
    }
    cout << g << ' ' << l << '\n';
    return 0;
}

Since C++17 you can also use std::gcd and std::lcm from <numeric>. Writing it yourself once is still worth doing so you know what is inside.

The LCM overflow trap

The obvious formula a * b / gcd(a, b) multiplies first. For two numbers near 1010 the product is 1020, which overflows long long even though the LCM itself might be small. Dividing first, a / gcd(a, b) * b, avoids the giant intermediate value. The final LCM can still be too large to store if the numbers share few factors, so check the problem's limits.

Handy properties

  • gcd(a, 0) = a, which makes 0 a safe starting value when taking the GCD of a list.
  • The GCD of a list can be computed one element at a time: gcd(a, b, c) = gcd(gcd(a, b), c).
  • gcd(a, b) = gcd(a, b − a). This lets you rewrite expressions like gcd(a1 + x, a2 + x, …) in terms of differences that do not depend on x, which is the whole trick behind Codeforces 1458A – Row GCD.
  • For factorials, gcd(A!, B!) is just min(A, B)!, because the smaller factorial divides the larger one. That is all Codeforces 822A – I'm bored with life asks for.

Practice problems

For more number theory, see the Lucky Division walkthrough.

Binary Exponentiation: Compute a^b mod m in O(log b)

Many problems ask for an answer “modulo 109 + 7”, and many of those need a large power such as 2n or ab with b up to 1018. Multiplying a by itself b times is hopeless. Binary exponentiation does it in about 60 multiplications.

The idea: square instead of multiply

Notice that a8 = ((a2)2)2. That is only three multiplications instead of seven. Repeated squaring gives us a, a2, a4, a8, a16, … very cheaply.

Any exponent can be written as a sum of powers of two, which is exactly its binary representation. For example 13 = 8 + 4 + 1, which is 1101 in binary. So

a13 = a8 × a4 × a1

We walk through the bits of b from the lowest. At each step, the current a holds a2k. If the bit is 1, multiply it into the result. Then square a and shift b right.

Tracing a = 3, b = 13

b (binary)lowest bitcurrent aresult
1101131 × 3 = 3
110093
111813 × 81 = 243
116561243 × 6561 = 1 594 323

And 313 is indeed 1 594 323, found with a handful of multiplications.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1'000'000'007;

// Computes (a^b) mod m in O(log b) multiplications.
long long power(long long a, long long b, long long m) {
    long long result = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) result = result * a % m;   // this bit of b is set
        a = a * a % m;                        // a, a^2, a^4, a^8, ...
        b >>= 1;
    }
    return result;
}

int main() {
    int q;
    cin >> q;
    while (q--) {
        long long a, b;
        cin >> a >> b;
        cout << power(a, b, MOD) << '\n';
    }
    return 0;
}

The input format matches CSES 1095 – Exponentiation: a number of queries, then a and b on each line.

Complexity

The loop runs once per bit of b, so O(log b) multiplications. For b up to 1018 that is at most 60 iterations.

Details that cause Wrong Answer

  • Overflow. Both factors are below 109 + 7, so their product is below about 1018, which fits in long long. With int it overflows. If the modulus is close to 1018, even long long is not enough and you need __int128.
  • Reduce a first. If a itself is larger than the modulus, take a %= m before the loop.
  • b = 0. Anything to the power 0 is 1, including 00 by the usual convention in these problems. The code starts result at 1 and handles it without a special case.

Where it shows up

  • Modular inverse. When the modulus p is prime, Fermat's little theorem gives a−1 ≡ ap−2 (mod p). That is how you “divide” under a modulus, for example when computing binomial coefficients.
  • Huge exponents. To compute abc mod p, reduce the exponent bc modulo p − 1 first (again by Fermat), then use binary exponentiation twice.
  • Matrix exponentiation. The same loop works with matrices instead of numbers, which computes the n-th Fibonacci number in O(log n).

Practice problems

Sieve of Eratosthenes: Find All Primes up to N Fast (C++)

Checking whether one number is prime is easy: try dividing it by everything up to its square root. But when a problem needs to know about every number up to 106 or 107, repeating that check for each number is too slow. The Sieve of Eratosthenes, which is more than two thousand years old, finds all of them at once.

The idea

Write down all numbers from 2 to n, and assume they are all prime. Then:

  1. Take the smallest number that is still marked prime. Call it p. It really is prime, because no smaller number divided it.
  2. Cross out every multiple of p: they have p as a factor, so they are composite.
  3. Move on to the next number that is still marked and repeat.

Whatever survives is prime.

Example with n = 30

  • p = 2: cross out 4, 6, 8, …, 30.
  • p = 3: cross out 9, 15, 21, 27 (12, 18, 24 and 30 were already gone).
  • p = 5: cross out 25 (30 was already gone).
  • p = 7: 7 × 7 = 49 > 30, so we can stop.

The survivors are 2, 3, 5, 7, 11, 13, 17, 19, 23, 29: ten primes.

Two small optimisations that matter

Start crossing at p × p. Any smaller multiple k × p with k < p has a prime factor smaller than p, so it was already crossed out earlier. For p = 5 above, 10, 15 and 20 were already gone, so we start at 25.

Stop the outer loop at √n. Once p × p > n, there is nothing left to cross out. Every composite number up to n has a prime factor at most √n, so it has already been handled.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    // is_prime[x] is true if x is prime, for 0 <= x <= n
    vector<bool> is_prime(n + 1, true);
    is_prime[0] = false;
    if (n >= 1) is_prime[1] = false;

    for (long long p = 2; p * p <= n; p++) {
        if (!is_prime[p]) continue;
        for (long long m = p * p; m <= n; m += p)
            is_prime[m] = false;
    }

    int count = 0;
    for (int x = 2; x <= n; x++)
        if (is_prime[x]) count++;
    cout << count << '\n';
    return 0;
}

The program prints how many primes there are up to n. For n = 106 it prints 78 498, and for n = 107 it prints 664 579. Once is_prime is built, each “is x prime?” question is a single array lookup.

Complexity

For each prime p, the inner loop does about n / p steps. Summing n/2 + n/3 + n/5 + … over primes gives O(n log log n), which is almost linear in practice. Memory is O(n). vector<bool> stores one bit per entry, so a sieve up to 107 needs only about 1.25 MB.

Common mistakes

  • Forgetting that 0 and 1 are not prime.
  • Writing p * p <= n with int when n is close to the int limit. The product can overflow, so use long long for p, as in the code above.
  • Re-running the sieve for every test case. Build it once, up to the largest value you will need.

A useful variation: smallest prime factor

Instead of a true/false flag, store for every number its smallest prime factor. Then you can factorise any number up to n in O(log n) by repeatedly dividing by its smallest prime factor. This is the standard way to factorise many numbers quickly.

Practice problems

For a gentler number theory warm-up, see Lucky Division (122A).

The Two Pointers Technique: Sliding Windows in O(n)

Many array problems ask about contiguous segments: the longest one with some property, the number of segments below a limit, and so on. Checking every segment takes O(n2) time. The two pointers technique often brings that down to O(n) by moving a window across the array instead of rebuilding it from scratch.

The problem

Valera has t free minutes and a row of n books. Book i takes ai minutes to read. He picks a starting book and reads books in order until he runs out of time (he never stops halfway through a book). What is the largest number of books he can read? This is Codeforces 279B – Books, with n up to 105.

In other words: find the longest contiguous segment whose sum is at most t.

Brute force first

Try every start l, extend r while the sum stays within t, and remember the best length. That is correct but O(n2) in the worst case, about 1010 steps here.

The key observation

Suppose the best window that ends at position r starts at position l. When we move to r + 1, the best window ending there cannot start before l: if it did, it would contain the window [l − 1, r], whose sum is already too large (all times are positive). So the left end only ever moves right.

That gives the algorithm:

  1. Move the right pointer one step and add the new book to the running sum.
  2. While the sum is more than t, remove the leftmost book and move the left pointer right.
  3. The window [left, right] is now the longest valid one ending at right. Update the answer.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    long long t;
    cin >> n >> t;
    vector<long long> a(n);
    for (auto &x : a) cin >> x;

    int best = 0;
    long long sum = 0;   // sum of a[left..right]
    int left = 0;
    for (int right = 0; right < n; right++) {
        sum += a[right];
        while (sum > t) {        // window too expensive: shrink from the left
            sum -= a[left];
            left++;
        }
        best = max(best, right - left + 1);
    }
    cout << best << '\n';
    return 0;
}

Walking through an example

t = 5, books 3 1 2 1.

  • right = 0: window [3], sum 3. Best = 1.
  • right = 1: window [3, 1], sum 4. Best = 2.
  • right = 2: sum 6 > 5, so drop the 3. Window [1, 2], sum 3. Best stays 2.
  • right = 3: window [1, 2, 1], sum 4. Best = 3.

Why it is O(n)

There is a while loop inside a for loop, which looks quadratic. But each pointer only moves forward, and each moves at most n times in total. Counting all the steps of both pointers gives at most 2n, so the whole algorithm is O(n) time and O(1) extra memory.

When two pointers works, and when it does not

The technique relies on the window property being monotonic: shrinking a valid window keeps it valid, and growing an invalid one keeps it invalid. With positive numbers and “sum ≤ t”, that holds.

It breaks if the array can contain negative numbers. Then adding an element can make the sum smaller, and the left pointer might need to move back. Those problems usually need prefix sums with sorting or a data structure instead.

Another common form: two pointers on a sorted array

To find two numbers in a sorted array that add up to x, start one pointer at each end. If their sum is too small, move the left pointer right; if too large, move the right pointer left. Each step rules out one element for good, so it is also O(n) after sorting. CSES 1640 – Sum of Two Values is a good exercise for this version.

Practice problems

Two pointers pairs naturally with run-counting problems like 266A, where you also compare each element with what came before.

Binary Search on the Answer: A Step-by-Step Guide with C++ Code

Most people learn binary search as “find a number in a sorted array”. In contests, the more powerful version is binary search on the answer: instead of searching an array, you search over possible answers and test each guess with a yes/no function. Once you see the pattern, a whole family of “minimum time” and “maximum size” problems becomes routine.

A motivating problem

A factory has n machines. Machine i takes ti seconds to make one product, and all machines work at the same time. What is the shortest time needed to make k products? Here n can be 2 × 105 and k and the ti up to 109. This is CSES 1620 – Factory Machines.

Computing the answer directly is awkward. But a different question is easy: “Is T seconds enough?” In T seconds, machine i makes ⌊T / ti⌋ products. Add those up and compare with k. That takes O(n).

The key property: monotonicity

If T seconds are enough, then T + 1 seconds are also enough. If T is not enough, nothing smaller is either. So if you write the answers to “is T enough?” for T = 0, 1, 2, … you get:

NO NO NO ... NO YES YES YES ...

We want the position of the first YES. A sequence that switches only once can be binary searched, exactly like a sorted array. Each check halves the range of possible answers, so even a range up to 1018 takes only about 60 checks.

A template that avoids off-by-one bugs

I use two variables with a fixed meaning:

  • lo is a value that is definitely NO.
  • hi is a value that is definitely YES.

While they are more than one apart, test the middle and move whichever end it belongs to. When the loop ends, hi is the first YES. Because the meaning of each variable never changes, there is no guessing about mid + 1 or mid - 1.

For this problem, lo = 0 works because zero seconds produce nothing. For hi, the fastest machine alone makes k products in min(t) × k seconds, so that is always enough.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int n;
long long k;
vector<long long> t;

// Can all machines together make at least k products in `time` seconds?
bool enough(long long time) {
    long long made = 0;
    for (long long ti : t) {
        made += time / ti;
        if (made >= k) return true;   // stop early to avoid overflow
    }
    return false;
}

int main() {
    cin >> n >> k;
    t.resize(n);
    for (auto &x : t) cin >> x;

    long long lo = 0;                                   // not enough
    long long hi = *min_element(t.begin(), t.end()) * k; // surely enough
    while (hi - lo > 1) {
        long long mid = lo + (hi - lo) / 2;
        if (enough(mid)) hi = mid;
        else lo = mid;
    }
    cout << hi << '\n';
    return 0;
}

Walking through an example

Machines take 3, 2 and 5 seconds, and we need k = 7 products.

  • T = 7: 2 + 3 + 1 = 6 products. Not enough.
  • T = 8: 2 + 4 + 1 = 7 products. Enough.

So the answer is 8. The binary search finds this in a handful of checks, starting from lo = 0 and hi = 2 × 7 = 14.

Complexity

Each check is O(n), and we do about log2(hi) checks, so O(n log V) where V is the upper bound. Here that is roughly 2 × 105 × 60 = 1.2 × 107 operations.

The overflow trap

hi can be 109 × 109 = 1018, so everything must be long long. Inside the check, adding up T / ti over 2 × 105 machines could exceed even that, which is why the function returns as soon as the count reaches k. Also compute the middle as lo + (hi - lo) / 2 instead of (lo + hi) / 2, so the sum does not overflow.

How to recognise these problems

  • The statement asks for a minimum value that makes something possible, or a maximum value that keeps something possible.
  • Checking one specific candidate is much easier than finding the best one directly.
  • Making the candidate “more generous” never turns a YES into a NO.

If all three hold, write the check function first and wrap it in the template above.

Practice problems

Prefix Sums Explained: Answer Range Sum Queries in O(1)

Prefix sums are one of the first techniques that make a beginner's solutions dramatically faster. The idea takes a minute to learn, and it turns up everywhere: range sums, counting problems, 2D grids, and as a building block inside harder algorithms.

The problem

You have an array of n numbers and q questions. Each question gives two positions l and r and asks for al + al+1 + … + ar. Both n and q can be 2 × 105.

Looping from l to r for each question costs up to n steps per question, so 4 × 1010 steps in the worst case. That is far too slow. We want each answer in constant time.

The idea

Build a second array where each entry stores the total of everything up to that point:

pre[0] = 0
pre[i] = a[1] + a[2] + ... + a[i] = pre[i - 1] + a[i]

Building it takes one pass. Now notice that the sum from l to r is “everything up to r” minus “everything before l”:

sum(l, r) = pre[r] − pre[l − 1]

That is one subtraction per question, no matter how wide the range is.

Worked example

Take the array 3 1 4 1 5 9 (positions 1 to 6).

i0123456
a[i]–314159
pre[i]034891423

The sum from position 3 to 5 is pre[5] − pre[2] = 14 − 4 = 10, and indeed 4 + 1 + 5 = 10. The sum of the whole array is pre[6] − pre[0] = 23.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, q;
    cin >> n >> q;

    // pre[i] = a[1] + a[2] + ... + a[i], with pre[0] = 0
    vector<long long> pre(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        long long x;
        cin >> x;
        pre[i] = pre[i - 1] + x;
    }

    while (q--) {
        int l, r;
        cin >> l >> r;
        cout << pre[r] - pre[l - 1] << '\n';
    }
    return 0;
}

The input format matches CSES 1646 – Static Range Sum Queries: n and q, then the array, then q lines with l and r.

Complexity

O(n) to build the prefix array and O(1) per query, so O(n + q) in total, with O(n) extra memory.

Why the extra zero matters

Starting the array with pre[0] = 0 and using 1-based positions means the formula works even when l = 1, because pre[l − 1] = pre[0] = 0. If you use 0-based indexing without that extra slot, you need a special case for queries starting at the first element, and special cases are where bugs live.

Common mistakes

  • Overflow. 2 × 105 values of up to 109 add up to 2 × 1014. Store prefix sums in long long even if the input fits in int.
  • Off by one. It is pre[r] − pre[l − 1], not pre[r] − pre[l]. The second version drops al.
  • Slow input. With 2 × 105 queries, unbuffered cin can be the bottleneck. The two lines at the top of main fix that.

Beyond sums

The same trick works for anything you can “undo”. Prefix XOR answers range XOR queries, because x XOR x = 0. Prefix counts answer questions like “how many even numbers are between l and r”: build an array that adds 1 for each position that satisfies the condition. In 2D, the sum of a rectangle can be found from four prefix values using inclusion–exclusion.

It does not work for minimum or maximum, because you cannot subtract a minimum. Those need other structures, such as a sparse table or a segment tree.

Practice problems

If you have not yet read it, my post on how to read constraints starts from the same “range sum” example and explains why 2 × 105 queries rule out the simple loop.

How to Read Constraints in Competitive Programming (and Pick the Right Complexity)

When I started solving problems on Codeforces, I read the story, thought about the logic, and only glanced at the numbers at the bottom of the statement. That is backwards. The constraints are often the biggest hint the problem setter gives you, because they tell you how fast your solution has to be, and that usually narrows down which technique you need.

This post is the habit I wish I had built on day one: read the limits first, estimate the operation count, and let that steer your idea.

The 108 rule of thumb

A typical judge machine runs roughly 108 simple operations per second in C or C++. It is not exact. Cache-friendly loops can do several times more, while heavy operations like map lookups or modulo do less. But it is close enough to decide between ideas. With a 1 second time limit, plan for at most a few times 108 basic steps.

Now plug the input size into the complexity you have in mind. If n = 2 × 105 and your idea is O(n2), that is 4 × 1010 steps, hundreds of times too slow. You know it will get Time Limit Exceeded before writing a single line.

A cheat sheet from n to complexity

Largest nComplexity that usually fitsTypical techniques
≤ 10O(n!), O(n6)Try every permutation
≤ 20O(2n · n)Bitmask brute force, subset enumeration
≤ 500O(n3)Floyd–Warshall, interval DP
≤ 5 000O(n2)Double loops, 2D DP
≤ 105 to 106O(n log n) or O(n)Sorting, binary search, prefix sums, two pointers, BFS/DFS, DSU
≤ 109 or moreO(log n), O(√n) or O(1)Math formula, binary exponentiation, binary search on the answer

The table is a starting point, not a law. A problem with n ≤ 2 × 105 almost never wants O(n2), but it might want O(n√n) or O(n log2 n).

Worked example: the same task at three sizes

Suppose you must answer q questions of the form “what is the sum of the array between positions l and r?”

  • n, q ≤ 100: just loop from l to r for each question. At most 104 steps.
  • n, q ≤ 5 000: the same loop is 2.5 × 107 steps. Still fine.
  • n, q ≤ 2 × 105: the loop could need 4 × 1010 steps. You need O(1) per question, which points straight at prefix sums.

Same story, three different intended solutions. The only thing that changed was the constraints.

Small limits are hints too

When a limit is surprisingly small, ask why. If n ≤ 20 in a problem about choosing items, the setter is telling you that trying all 220 ≈ 106 subsets is allowed. If values are at most 100 while n is large, maybe you should count how often each value appears instead of sorting. If k ≤ 10 while everything else is large, the solution probably has a factor of k or 2k in it.

Watch the value limits as well

Constraints do not only describe time. They also tell you which types to use:

  • A 32-bit int holds up to about 2.1 × 109. If you add up 105 values of size 109, the sum can reach 1014, so use long long.
  • Multiplying two numbers below 109 + 7 gives up to about 1018, which fits in long long (limit about 9.2 × 1018) but not in int.
  • Memory limits matter too. An array of 108 ints is 400 MB, which is more than most problems allow.

Theatre Square (1A) is the classic example: the logic is one line, but the answer can be 1018, and using int is the most common reason for Wrong Answer.

A routine to use on every problem

  1. Read the input limits and the time limit before the story.
  2. Write down the largest n and look it up in the table above.
  3. Think of the simplest correct solution and estimate its operation count.
  4. If it fits, write it. If not, you now know how much faster the real idea must be.
  5. Check the value limits and pick int or long long accordingly.

It takes thirty seconds and saves a lot of wasted submissions. If you are still deciding whether contests are worth your time at all, I wrote about why competitive programming is worth it.

Sunday, July 26, 2026

The Algorithmic Crucible: Why Competitive Programming is Essential for CSE Students

The Algorithmic Crucible: Why Competitive Programming is Essential for CSE Students

Walk into any university computer science department, and you will find a sharp divide. On one side, students who view Competitive Programming (CP) as the ultimate benchmark of coding prowess. On the other, those who argue it is an abstract sport with little relevance to building real-world software like mobile applications, REST APIs, or automated deployment pipelines.

Having evaluated countless algorithms both in academic settings and in production codebases, I want to bridge this gap. What is the actual ROI of grinding LeetCode, Codeforces, or HackerRank for a Computer Science and Engineering (CSE) student?


1. The Shift from "Working Code" to "Optimal Code"

In standard academic coursework, the goal is often simply to make the program work. If a student writes an $O(N^2)$ algorithm for a dataset of 100 items, the code compiles, the tests pass, and they get an A.

Competitive programming aggressively breaks this habit. When a platform throws a dataset of $N = 10^5$ at that same algorithm, it results in a brutal Time Limit Exceeded (TLE). CP forces the brain to immediately evaluate the mathematical constraints of a problem before writing a single line of code. You learn to instinctively map constraints to complexities: seeing $N = 10^5$ instantly tells you that your solution must be bounded by $O(N \log N)$ or $O(N)$. This translates directly to backend engineering, where a poorly optimized database query can crash a web server under load.

2. Edge Case Conditioning and Defensive Programming

Software engineers spend a significant portion of their time debugging edge cases. What happens if the array is empty? What if all elements are negative? What if the integer overflows a 32-bit boundary?

In CP, failing to account for an edge case results in a Wrong Answer (WA). Over time, competitive programmers develop a hyper-awareness of boundary conditions. This psychological conditioning creates developers who write highly defensive, resilient code. When you are configuring complex deployment pipelines or handling asynchronous state management in a mobile app, this ability to predict the "unhappy paths" saves hundreds of hours of production debugging.


3. Advanced Data Structure Fluency

Most CSE curriculums teach Trees, Graphs, and Hash Maps. However, knowing how a Segment Tree works in theory is very different from deploying one to solve a range-query problem under a 2-second time limit.

Data Structure CP Application Industry Translation
Tries (Prefix Trees) String manipulation, XOR maximums Search engine autocompletion, network routing
Disjoint Set (Union-Find) Cycle detection, dynamic connectivity Image processing, clustering algorithms in ML
Priority Queues (Heaps) Dijkstra’s algorithm, dynamic median Task scheduling, load balancing in servers

4. The "Big Tech" Interview Reality

We cannot ignore the pragmatic reason for CP: career trajectory. The technical interview loops for FAANG and top-tier global tech companies are heavily indexed on algorithmic problem solving. A strong background in CP essentially trivializes the standard technical interview. It allows the candidate to bypass the stress of the algorithmic round and focus on demonstrating communication skills and systems design knowledge.


The Caveat: CP is Not Software Engineering

As a professor and a practitioner, I must offer a crucial warning: Competitive programming is not a substitute for software engineering.

CP code is often written to be disposable—it is a script designed to run once, output a result, and terminate. Variables are named x, y, and z. Everything is crammed into a single file.

Real-world software engineering requires:

  • Designing maintainable, scalable architectures (like MVC or MVVM).
  • Writing clean, documented, and testable code.
  • Collaborating via Git and managing CI/CD pipelines.
  • Navigating complex frameworks, database migrations, and client requirements.

The Verdict

For a CSE student, competitive programming is like lifting weights for an athlete. A football player doesn't bench press on the field during a game, but the raw strength built in the gym makes them a vastly superior player. Treat CP as your algorithmic gym. Build your analytical strength there, but ensure you also step onto the field by building real projects, engaging in code reviews, and mastering modern development frameworks.

How much time do you allocate between competitive programming and project building? Share your thoughts and strategies in the comments below.

Deep Dive into Breadth-First Search (BFS): Graph Theory & Python Implementation

Deep Dive into Breadth-First Search (BFS): Graph Theory and Pragmatic Implementation

As both an educator and a software engineer, I often see students and developers treat graph traversal algorithms as mere academic exercises. However, understanding the topological mechanics of Breadth-First Search (BFS) is critical for designing scalable systems—from state management in mobile architectures to optimizing web crawler pipelines.

Today, we are taking an in-depth look at BFS. We will bridge the gap between formal graph theory and production-grade engineering, examining the mathematics, the algorithmic complexity, and the memory management trade-offs.


1. The Theoretical Foundation

In graph theory, we define a graph mathematically as $G = (V, E)$, where $V$ represents a set of vertices (nodes) and $E$ represents a set of edges connecting them. BFS is a traversal algorithm that explores the graph's state-space tree level by level.

Instead of aggressively plunging into the depth of a graph (as Depth-First Search does), BFS radiates outward from a source vertex $s$. It discovers all vertices at distance $k$ from $s$ before discovering any vertices at distance $k + 1$. This specific frontier-expansion mechanism is why BFS inherently computes the shortest path in an unweighted graph.

2. Algorithmic Mechanics and The Queue

The operational core of BFS is a Queue—a First-In-First-Out (FIFO) data structure. The algorithm maintains a "frontier" of nodes. As a node is dequeued, its adjacent, unvisited neighbors are enqueued.

The Critical Engineering Nuance: Queue Choice

A common mistake in Python implementations is using a standard list as a queue and calling pop(0). From a systems perspective, a standard array-backed list requires shifting all subsequent elements in memory when the first element is removed. This turns an $O(1)$ operation into an $O(N)$ operation, inadvertently degrading the entire algorithm's time complexity to $O(|V|^2)$.

In production, we must use a double-ended queue (like Python's collections.deque), which is implemented as a doubly linked list, ensuring $O(1)$ time complexity for both appends and pops.


3. Production-Grade Implementation (Python)

Below is a robust, type-hinted implementation using an adjacency list and a proper deque object.


from collections import deque
from typing import Dict, List, Set, Any

def bfs_optimal(graph: Dict[Any, List[Any]], start_node: Any) -> None:
    """
    Executes a Breadth-First Search on a graph.
    
    Args:
        graph: Adjacency list representation of the graph.
        start_node: The vertex from which traversal begins.
    """
    # Use a Set for O(1) lookups. Lists have O(V) lookup time.
    visited: Set[Any] = set([start_node])
    
    # Initialize a double-ended queue for O(1) popleft operations
    queue: deque = deque([start_node])

    while queue:
        # O(1) operation
        current_node = queue.popleft()
        print(current_node, end=" -> ")

        # Iterate through adjacent vertices
        for neighbor in graph.get(current_node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
                
# Define the graph G = (V, E)
adjacency_list = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': ['F'],
    'F': []
}

print("Initiating Level-Order Traversal:")
bfs_optimal(adjacency_list, 'A')
print("END")

4. Complexity Analysis (Big-O)

Time Complexity: $O(|V| + |E|)$
In an adjacency list representation, the algorithm visits every vertex ($V$) exactly once and examines every edge ($E$) exactly once. Note: If you use an Adjacency Matrix instead, the time complexity degrades to $O(|V|^2)$ because you must scan an entire row of length $V$ for every vertex.

Space Complexity: $O(|V|)$
The memory footprint is determined by the maximum size of the queue. In the worst-case scenario (a highly branched tree or a star graph), the queue will hold all the leaf nodes simultaneously. For a tree, this width is roughly $|V|/2$, dropping the constant gives us $O(|V|)$.


5. Advanced Use Cases in Software Architecture

Beyond simple traversals, BFS is the algorithmic engine behind several complex computational tasks:

  • Edmonds-Karp Algorithm: BFS is used to find the shortest augmenting paths in flow networks to compute the maximum flow.
  • Garbage Collection (Cheney's Algorithm): Used in memory management, BFS efficiently traverses object references to separate live objects from garbage, avoiding the deep recursion stack overflows that DFS might cause.
  • Bipartite Graph Checking: By attempting to color a graph using two alternating colors layer by layer, BFS can determine if a graph is bipartite in linear time.
  • Peer-to-Peer Networks (BitTorrent): BFS logic is used to broadcast data packets to neighboring nodes efficiently without routing loops.

6. The Engineering Trade-offs: BFS vs. DFS

While BFS guarantees finding the shortest path, it comes with a structural cost:

Metric BFS DFS
Memory Overhead High. Stores entire frontier ($O(|V|)$ width). Low. Stores only the current path ($O(d)$ depth).
Cache Locality Poor. Jumps horizontally across memory references. Excellent. Follows pointers sequentially downward.
Best For... Shortest path, level-order mapping. Topological sorting, cycle detection, deep search.

Final Thoughts

Understanding when to deploy BFS over DFS is a hallmark of strong systems design. If the target is close to the source, or if you need the absolute shortest sequence of operations (unweighted), BFS is your tool. However, if memory constraints are strict, or if the target is buried deep within a dense graph, alternative heuristics or bounded depth-first approaches might be required.

What are your preferred methods for optimizing graph traversals in your tech stack? Drop a comment below to discuss.

Friday, July 24, 2026

Flutter vs React Native: Which Cross-Platform Framework Should You Choose in 2026?

 If you're planning to build a mobile app in 2026, chances are you've narrowed your options down to Flutter and React Native. Both frameworks let developers create apps for Android and iOS from a single codebase, saving time and development costs.

But which one is the better choice?

The answer depends on your project, your team's experience, and your long-term goals. Let's compare the two frameworks in the areas that matter most.

What is Flutter?

Flutter is Google's open-source UI toolkit. It uses the Dart programming language and comes with its own rendering engine, allowing developers to create highly consistent and visually appealing applications across platforms.

Flutter is known for smooth animations, customizable widgets, and excellent performance.

What is React Native?

React Native is an open-source framework created by Meta. It allows developers to build mobile applications using JavaScript or TypeScript with React.

Instead of drawing every UI element itself, React Native communicates with native platform components, making it a great choice for teams already familiar with React.


Performance

Flutter generally offers better performance because it compiles directly to native code and renders its own interface. This results in smoother animations and more consistent performance across devices.

React Native also performs well, especially for typical business applications, but very complex animations or graphics-intensive apps may require additional optimization.

Winner: Flutter


Learning Curve

If you're already experienced with JavaScript or React, React Native is easier to learn.

Flutter requires learning Dart, which is relatively simple but still another language to master.

Winner: React Native


User Interface

Flutter provides a rich collection of built-in widgets that make it easy to create beautiful, consistent interfaces.

React Native relies more on native components, giving apps a platform-specific look and feel with less customization out of the box.

Winner: Flutter


Development Speed

Both frameworks support Hot Reload, allowing developers to see changes almost instantly.

React Native benefits from the massive JavaScript ecosystem, while Flutter offers many official packages and excellent tooling.

For experienced developers, productivity is high with both.

Winner: Tie


Community and Ecosystem

React Native has been around longer and benefits from the enormous JavaScript and React communities.

Flutter's community has grown rapidly, and most common packages are actively maintained.

Winner: React Native (slightly)


Maintenance

Flutter's ecosystem is more consistent because Google controls much of the framework and widget library.

React Native projects sometimes require extra attention when updating dependencies or integrating native modules.

Winner: Flutter


Best Use Cases

Choose Flutter if you:

  • Want highly polished UI and animations
  • Need consistent design across Android and iOS
  • Are building a startup MVP
  • Plan to support desktop or web in the future
  • Value predictable performance

Choose React Native if you:

  • Already know React or JavaScript
  • Have a web development team using React
  • Need to share knowledge across web and mobile
  • Want access to the vast JavaScript ecosystem

Quick Comparison




Final Verdict

There is no universal winner because both frameworks are mature and capable.

If your priority is performance, modern UI, and long-term consistency, Flutter is an excellent choice.

If your team already works with React and JavaScript, React Native can help you move faster with familiar tools.

Ultimately, the best framework is the one that aligns with your team's skills, project requirements, and future maintenance plans—not just the latest trend.

AI in Software Development: Pros and Cons (2026 Guide for Developers)

Artificial Intelligence (AI) has become one of the biggest talking points in software development. Whether you're a student writing your first "Hello, World!", a freelancer building client projects, or a senior software engineer leading a team, you've probably used an AI tool like ChatGPT, GitHub Copilot, Claude, or Gemini.

But with all the excitement comes an important question:

Is AI making developers more productive, or is it slowly replacing them?

The truth lies somewhere in the middle.

The Advantages of AI in Software Development

1. Faster Development

AI can generate boilerplate code, create APIs, write database queries, and even build complete UI components in seconds. Tasks that previously took hours can now be completed much faster.

This doesn't mean developers become unnecessary—it means they spend less time on repetitive work and more time solving real problems.

2. Better Learning Experience

For beginners, AI acts like a patient tutor.

Instead of spending hours searching forums, developers can ask questions, request examples, or get explanations tailored to their level of understanding.

Learning technologies like Flutter, React, Python, or .NET has become much more approachable.

3. Improved Code Quality

Modern AI tools can help:

  • Detect bugs
  • Suggest optimizations
  • Recommend best practices
  • Explain complex code
  • Generate unit tests
  • Review pull requests

While AI isn't perfect, it often catches mistakes before they become costly.

4. Increased Productivity

Many software engineers report saving several hours each week by using AI for:

  • Documentation
  • Writing SQL queries
  • API integration
  • Refactoring code
  • Debugging
  • Writing regular expressions

These time savings allow developers to focus on architecture, business logic, and user experience.

5. Better Documentation

Let's be honest—most developers don't enjoy writing documentation.

AI can generate:

  • README files
  • API documentation
  • Code comments
  • User guides
  • Technical documentation

Good documentation makes projects easier to maintain and onboard new team members.


The Disadvantages of AI in Software Development

1. AI Can Be Wrong

One of the biggest risks is trusting AI blindly.

AI may generate code that:

  • Doesn't compile
  • Contains security vulnerabilities
  • Uses outdated libraries
  • Ignores business requirements
  • Produces inefficient algorithms

Always review AI-generated code before deploying it.

2. Reduced Problem-Solving Skills

If developers rely on AI for every challenge, they may stop thinking critically.

Programming isn't just about writing code—it's about analyzing problems and designing solutions.

Overdependence on AI can weaken these essential skills over time.

3. Security Risks

Sharing confidential code or sensitive business logic with AI services may violate company policies or compliance requirements.

Organizations should establish clear guidelines for using AI responsibly.

4. Lack of Context

AI doesn't fully understand:

  • Company goals
  • Business rules
  • User expectations
  • Team conventions
  • Long-term architecture

It generates responses based on patterns, not true understanding.

That's why human oversight remains essential.

5. Job Market Changes

AI is changing the hiring landscape.

Companies now expect developers to:

  • Use AI effectively
  • Deliver faster
  • Learn new technologies quickly
  • Solve more complex problems

Rather than replacing skilled developers, AI is increasing the expectations placed on them.


Skills That AI Cannot Easily Replace

Despite rapid advancements, some skills remain uniquely human:

  • System architecture
  • Product thinking
  • Communication
  • Leadership
  • Creative problem-solving
  • Client interaction
  • Understanding business needs
  • Making engineering trade-offs

These abilities become even more valuable in an AI-powered world.


Best Practices for Using AI

Here are a few practical tips:

  • Treat AI as an assistant, not an authority.
  • Verify every important piece of generated code.
  • Learn the fundamentals before relying on AI.
  • Protect sensitive company information.
  • Continue improving your programming and communication skills.

The developers who thrive won't be those who avoid AI—they'll be the ones who know when to trust it and when to question it.


Final Thoughts

AI is transforming software development at an incredible pace. It helps developers work faster, learn more efficiently, and automate repetitive tasks. At the same time, it introduces new challenges related to accuracy, security, and overreliance.

The future isn't AI versus developers.

It's developers who use AI effectively versus those who don't.

As software engineering continues to evolve, the most successful professionals will combine strong technical fundamentals with the smart use of AI tools. In that sense, AI isn't replacing developers—it's changing what it means to be a great one.

#cfsolv

----------------

Md Rahat
Software Engineer(App),

NECX Inc.

Sunday, January 5, 2025

Mobile App Development & It's Importance

Mobile App Development: The Future of Technology

In today’s fast-paced digital world, mobile app development has become one of the most crucial aspects of businesses, startups, and entrepreneurs. The rise of smartphones has significantly changed the way we interact with the world. Mobile apps offer a streamlined way to engage with customers, enhance business processes, and even build personal brand experiences. Let’s dive into the world of mobile app development and explore how it can take your business to new heights.

Why Mobile Apps Matter?

Mobile applications are not just about providing convenience—they are a direct way to reach your audience. With millions of mobile users worldwide, having a mobile app can dramatically increase your visibility, user engagement, and profitability. Whether it’s for e-commerce, entertainment, or social media, apps can fulfill a wide variety of business needs.

Some of the key reasons why mobile app development is important include:

  • Increased User Engagement: Mobile apps offer faster access to services and features, providing a more direct user experience.
  • Improved Brand Recognition: A well-designed mobile app helps increase brand visibility.
  • Better Customer Service: Apps make it easier for customers to interact with your business, resolve issues, and provide feedback.
  • Data Collection & Analytics: Mobile apps allow businesses to collect valuable customer data and provide insights to improve services and products.

PNC Soft Tech: Your Trusted Mobile App Development Partner

PNC Soft Tech is a software company that specializes in developing high-quality web and mobile applications. They understand the intricacies of app development, whether it’s for business or personal use. With their expertise, they create apps that enhance user experience, boost brand recognition, and streamline business operations.

Visit PNC Soft Tech to explore how their mobile app development services can help elevate your business to the next level. You can also reach out to them directly via WhatsApp at +8801793278360.

The Mobile App Development Process

Developing a mobile app involves several stages, from ideation and planning to design, development, and finally, deployment. Below are the essential steps involved:

  1. Idea and Conceptualization: Before development, it’s crucial to outline the goals, target audience, and features of the app. A strong idea forms the foundation for a successful app.
  2. Designing the User Interface (UI): A user-friendly interface is critical for success. The design should be intuitive, clean, and engaging to ensure a pleasant user experience.
  3. Development: This stage involves writing the actual code for the app. The development can be divided into:
    • Frontend Development: This is the part of the app that users interact with. It includes the design elements and user flow.
    • Backend Development: This deals with the server-side components and databases that power the app’s features.
  4. Testing: After development, it’s essential to test the app for bugs and issues. Quality assurance ensures the app functions smoothly across devices.
  5. Launch and Maintenance: Once the app is tested and ready, it’s time for deployment to app stores. Even after launch, apps need continuous updates and maintenance to improve functionality and keep up with evolving technologies.

PNC Soft Tech: Quality Web & Mobile Applications

Looking to build your own mobile app? PNC Soft Tech provides professional mobile app development services designed to create solutions that are not only functional but also scalable. Their team has experience working with both iOS and Android platforms, ensuring that your app reaches a wider audience.

Check out PNC Soft Tech to learn more about their offerings, or get in touch with them via WhatsApp at +8801793278360 for a consultation.

Trends in Mobile App Development

With the tech world constantly evolving, mobile app development trends also change. Staying up-to-date with the latest trends can help businesses create innovative apps that stand out. Some of the latest trends in mobile app development include:

  • Artificial Intelligence & Machine Learning: AI and ML are being integrated into mobile apps to enhance user experiences, such as personalized recommendations and intelligent chatbots.
  • Augmented Reality (AR) & Virtual Reality (VR): AR and VR are revolutionizing mobile gaming and retail apps, offering immersive experiences.
  • Cross-Platform Development: Frameworks like Flutter and React Native allow developers to build apps for both iOS and Android, saving time and resources.
  • 5G Connectivity: With the rollout of 5G networks, apps will become faster and more capable of handling complex tasks, including high-quality video streaming and gaming.

PNC Soft Tech: Innovating the Future of Mobile Apps

PNC Soft Tech is committed to delivering high-performance mobile applications with cutting-edge technology. Whether you're looking to develop an app for your business or a personal project, they offer tailored solutions to meet your specific needs.

Visit PNC Soft Tech to get started or contact them through WhatsApp at +8801793278360 for further inquiries.

Conclusion

Mobile app development is a key factor in how businesses connect with their customers. By understanding the development process and embracing the latest trends, companies can create apps that engage users and drive business growth. With the help of trusted developers like PNC Soft Tech, you can bring your app ideas to life and make a lasting impact in the mobile space.

Explore the services offered by PNC Soft Tech to create high-quality web and mobile applications for your business today!