Showing posts with label algorithm. Show all posts
Showing posts with label algorithm. Show all posts

Jan 1, 2026

[algorithm] minhash

Jaccard Similarity



The Conceptual Process
  • Shingling: Convert documents into sets of short strings (e.g., 3-word phrases).
  • Permutation: Imagine taking all possible shingles and randomly shuffling their order.
  • The "Min" Hash: The MinHash value for a document is the index of the first shingle that appears in that document after the shuffle.
The probability that two sets have the same MinHash value is exactly equal to their Jaccard Similarity:


Benefit

  • Efficiency: we can compare two 1MB documents by just comparing 100 integers (their MinHash signature).
  • Scalability: It is often paired with Locality Sensitive Hashing (LSH) to find similar items in sub-linear time, meaning we don't have to check every single pair in a database.
  • Storage: only need to store the compact signatures, not the full text of the documents.

Real-World Applications

  • Search Engines: Detecting "mirror" websites or slightly modified versions of the same article.
  • Genomics: Comparing DNA sequences to find similar species or genes.
  • Plagiarism Detection: Finding overlapping blocks of text across a massive database of student essays.



#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <limits>

using namespace std;

// A simple hash function to simulate different permutations
// h(x) = (a * x + b) % c
uint32_t hash_func(uint32_t x, uint32_t a, uint32_t b, uint32_t c) {
    return (a * x + b) % c;
}

int main() {
    // 1. Represent two documents as sets of "shingles" (hashed strings)
    set<uint32_t> doc1 = {10, 25, 30, 45, 50};
    set<uint32_t> doc2 = {10, 25, 31, 46, 50}; // Mostly similar

    // 2. Define parameters for our MinHash signatures
    int num_hashes = 100;
    uint32_t large_prime = 1000003;

    // Coefficients for our hash functions (randomly chosen for this example)
    vector<uint32_t> a_coeffs(num_hashes), b_coeffs(num_hashes);
    for(int i = 0; i < num_hashes; i++) {
        a_coeffs[i] = rand() % 500 + 1;
        b_coeffs[i] = rand() % 500 + 1;
    }

    // 3. Generate Signatures
    vector<uint32_t> sig1(num_hashes, numeric_limits<uint32_t>::max());
    vector<uint32_t> sig2(num_hashes, numeric_limits<uint32_t>::max());

    for (int i = 0; i < num_hashes; i++) {
        // Find minimum hash for doc1
        for (uint32_t shingle : doc1) {
            sig1[i] = min(sig1[i], hash_func(shingle, a_coeffs[i], b_coeffs[i], large_prime));
        }
        // Find minimum hash for doc2
        for (uint32_t shingle : doc2) {
            sig2[i] = min(sig2[i], hash_func(shingle, a_coeffs[i], b_coeffs[i], large_prime));
        }
    }

    // 4. Estimate Similarity
    int matches = 0;
    for (int i = 0; i < num_hashes; i++) {
        if (sig1[i] == sig2[i]) matches++;
    }

    double estimated_jaccard = (double)matches / num_hashes;
    cout << "Estimated Jaccard Similarity: " << estimated_jaccard << endl;

    return 0;
}

Oct 4, 2025

[alrotighm] trampoline pattern

using trampoline to deal with recursive stack overflow situation(while tail-call is not applicable):
#include <iostream>
#include <vector>
#include <numeric>
#include <variant>
#include <functional>
#include <utility>

// --- (Paste the Bounce, Step, and trampoline definitions from above here) ---

template<typename T>
struct Bounce;

template<typename T>
using Step = std::variant<T, Bounce<T>>;

template<typename T>
struct Bounce {
    std::function<Step<T>()> thunk;
};

template<typename T>
T trampoline(Step<T> first_step) {
    Step<T> current_step = std::move(first_step);
    while (std::holds_alternative<Bounce<T>>(current_step)) {
        current_step = std::get<Bounce<T>>(current_step).thunk();
    }
    return std::get<T>(current_step);
}

// --- (Paste the sum_trampolined function from above here) ---

Step<long> sum_trampolined(const std::vector<long>& data, size_t index, long current_sum) {
    if (index == data.size()) {
        return current_sum;
    }
    return Bounce<long>{
        [=]() {
            return sum_trampolined(data, index + 1, current_sum + data[index]);
        }
    };
}


int main() {
    // This will now work without crashing!
    std::vector<long> large_vec(200000, 1);

    // To start the process, we create the very first step.
    Step<long> first_step = sum_trampolined(large_vec, 0, 0);

    // The trampoline function runs the computation to completion.
    long total = trampoline(first_step);

    std::cout << "Trampolined sum of large vector: " << total << std::endl;
    std::cout << "The program finished successfully." << std::endl;

    return 0;
}

Oct 2, 2025

[Algorithm] branchless binary search

template <class ForwardIt, class T, class Compare>
ForwardIt branchless_lower_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp)
{
    auto length = last - first;

    while (length > 0)
    {
        auto half = length / 2;
        // multiplication (by 1) is needed for GCC to generate CMOV
        // comp returns 1 if value > first[half], returns 0 otherwise
        first += comp(first[half], value) * (length - half); 
        length = half;
    }

    return first;
}

Feb 12, 2025

[Algorithm] Fowler–Noll–Vo (FNV) hash


static uint64_t FNVhash64(uint64_t val) {
  const uint64_t FNV_offset_basis_64 = 0xCBF29CE484222325L;
  const uint64_t FNV_prime_64 = 1099511628211L;

  uint64_t hashval = FNV_offset_basis_64;
  for (int i = 0; i < 8; i++) {
    hashval = hashval ^ (val & 0xff);
    hashval = hashval * FNV_prime_64;
    val >>= 8;
  }
  return (hashval & 0x8000000000000000) ? -hashval : hashval;
}

Jul 8, 2022

[algorithm] hot key detection

An algorithm/data structure for hot-key detection

Reference:


Requirement

  1. The QPS threshold is defined as such that there are limited keys are hot in the defined time range.
  2. Hot keys should be detected in less than 30 seconds, without significant CPU, memory overhead, or latency increase to the system.


Input

Hashed Key (or itself should be unique)


Output

Hot index of the key.


Idea

  • Using 2 level filter.
  • Each level is a data structure that utilizes CPU pre-fetch.
  • We choose std::vector in this case.
  • The size of std::vector is predefined; no runtime adjustment allowed.


Level-1 vector(std::vector<int>)

hash function

/**
 * bucketsMask: bucket number - 1; bucket number should be the power of 2.
 * i.e. assert((numBuckets & (numBuckets - 1)) == 0);
 * Thus the bucketsMask would become 0x111...111
 * This indicates how many buckets and mimic consistent hash ring nodes.
 *
 */
size_t l1HashFunction(uint64_t hash) const { return hash & bucketsMask; }
Level-1 vector counter result is used as lever to detect if proceed to Level-2.
By proceeding to Level-2 means:
  1. If level-1 counter value < (l1Threshold/2); return 0.
  2. If level-1 counter > l1Threshold, return level-2 counter result
  3. Otherwise, insert the input key/hash value to level-2 counter if there's
    open space left. We check for up to defined bucket to see if there are available slot.
  4. Return result which is normalized between (1 ~ 255).


Level-2 vector(std::vector<L2Record>)

hash function

size_t l2HashFunction(uint64_t hash) const {
    return (hash * 351551 /* prime */) & bucketsMask;
}
Create another "Base Nodes" counts based on input hash value.


Data structure

struct L2Record {
    uint64_t hash{0};
    uint32_t count{0};
    uint32_t hashHits{0};
};

At this stage, we didn't record the 'exact' input key; thus we use L2Record's hash to assign the exact input key/hash. 
L2Record's count is the Level-2 l2HashFunction "Base node" count; it's the count of multiple input key/hash value that has the same l2HashFunction value. 
And if L2Record's count < hotnessMultiplier; we exit early with current L2Record's count result. Otherwise we decided to insert the input key/hash value as a new entry to the Level-2 count.

hotnessMultiplier can be 4 or 6.

There are chances input key/hash value will not be recorded/inserted due to there are
more than certain keys are hot; which violates the contract of this algorithm.


Now we can focus the maintenance of the data structure.
The concept of 'decay' is used and the algorithm does not use extra thread for the clean up due to avoiding lock which reduces the performance.
We check the maintenance threshold for each API call.

Maintenance comes in 4 parts:
  1. move holes
  2. decay Level-1 and Level-2 data structure value.
  3. update l1Threshold_ base on numbers of the current non-zero value in Level-2
    data structure(i.e. std::vector<L2Record>)
  4. The maintenance threshold is calculated at runtime based on current l1Threshold_.

Jun 13, 2022

[C++][CPPCON] s.t. about type kinds

Reference:
https://www.youtube.com/watch?v=va9I2qivBOA


std::variant
all_of
any_of
mismatch
equal
merge
set_union
set_intersection


Packs are a distinct kind

All types belong to a kind
 ... or 14(types), depending how you count
e.g.
nullptr
template names belong to another kind




There is a one-of-a-kind construct
Adding new kinds is almost unprecedented.

Packs are unlike everything else in C++
pointer to member function,
i.e.  ->*  .* return has no type.

std::integer_sequence


Hybrid algorithm, compile-time + runtime:
i.e. Linear search at compilation, binary search at runtime.

Jun 11, 2022

[algorithm] fast string hash function

Reference: 
http://www.cse.yorku.ca/~oz/hash.html

auto hashed = [] (const char* str) consteval {
    std::size_t hash = 5381;
    while (*str != '\0') {
        hash = hash * 33 ^ *str++;
    }
    return hash;
};

// requires hashed() in compile-time context
enum class drinkHashes : long {
	beer = hashed("beer"),
    wine = hashed("wine"),
    water = hashed("water"),
};

std::array arr{hashed("beer"), hashed("wine"), hashed("water")};

Apr 28, 2022

[C++][Algorithm][design] predicate callable logic for sorting (and any of the ordering function/algorithm call)

Reference:
https://danlark.org/2022/04/20/changing-stdsort-at-googles-scale-and-beyond/
https://vsdmars.blogspot.com/2018/06/c-regular-type.html

Concept:

Type:


Design by contract (link to defensive programming)

P: precondition
Q: operation
R: postcondition


Domain (as in math)

Domain of operation is used in the ordinary math sense to denote the set of values over which an operation is (required to be) defined.

This set can change over time. Each component may place additional requirements on the domain of an operation.

These requirements can be inferred from the uses that a component makes of the operation and are generally constrained to those values accessible through the operation's arguments.

Domain of the operation is NOT the types of the arguments.

 

Safety & Correctness

  • An operation is safe if it cannot lead to UB
    • directly or indirectly
    • even if the operation preconditions are violated
  • An unsafe operation may lead to UB if preconditions ever are violated
    • Either directly or during subsequent operations, safe or not
Code that violates preconditions is incorrect.


Requirements for correctness

  • A correctly implemented operation guarantees that:
    • If preconditions are satisfied
      • The operation will either succeed, result matches post conditions
      • Or report failure, return an error, thrown an exceptions, set errno etc.
      • Any objects being mutated by the operations must be left in a "known or determinable state"
        • A weaker requirement than valid
    • If preconditions is not satisfied
      • If the operation is safe
        • The result is unspecified which could include:
          • Failure
          • Trapping
          • Leaving any object being mutated by the operations in an unspecified, possibly invalid state.
      • If the operations is unsafe
        • The behavior is undefined(Full STOP)
Compiler can do /anything/ if there's an UB(stripping expressions etc.)

We could exploit contract to work for us; e.g. unsigned (contracted with mod(2^b))


Strong preconditions

  • Pros
    • flexibility of implementation
    • ascribe meaning and intent to an operation
    • simplify requirements and reasoning about code
  • Cons
    • limit clever uses that exploit otherwise defined behavior
    • allow for variance in behavior between implementations

ALWAYS refer to C++ ISO for contract of std::


A tl;dr take away for predicator of sorting

When calling any of the ordering functions including
  • std::sort
  • std::find
compare functions(aka predicate) much comply with the strict weak ordering which formally means the following:
  • Irreflexivity: x < x is false (strict partial order rule)
  • Asymmetry: x < y and y < x cannot be both true (strict partial order rule)
  • Transitivity: x < y and y < z imply x < z (strict partial order rule)
  • Transitivity of incomparability: x == y and y == z imply x == z, where x == y means x < y and y < x are both false (equivalence relations on incomparable elements rule)
Above conditions are used for optimization purposes and an abide by is a must for code correctness.



Feb 11, 2022

[C++][c++20] std::midpoint

Reference:
https://devblogs.microsoft.com/oldnewthing/20220207-00/?p=106223
https://ai.googleblog.com/2006/06/extra-extra-read-all-about-it-nearly.html
https://en.cppreference.com/w/cpp/numeric/midpoint
https://www.youtube.com/watch?v=sBtAGxBh-XI

Types for std::midpoint
  1. integral
    5 of them:
    signed char, short int, int, long int, long long int https://en.cppreference.com/w/cpp/language/types
    signed overflow is UB; unsigned overflow is defined to wrap around.
    signed converted to unsigned is always safe.
    every signed integral type has a corresponding unsigned integral type and its name is std::make_unsigned_t<T>
  2. pointer
  3. floating point
    Denormalized numbers, INF, and NaN
    "at most one inexact operation"


No, don't do this:
unsigned average(unsigned a, unsigned b)
{
    return (a + b) / 2;
}

Better if we know which value is larger:
unsigned average(unsigned low, unsigned high)
{
    return low + (high - low) / 2;
}

Better:
unsigned average(unsigned low, unsigned high)
{
	return ((unsigned int)low + (unsigned int)high)) >> 1;
}

Better:
unsigned average(unsigned a, unsigned b)
{
    return (a / 2) + (b / 2) + (a & b & 1);
}

Better:
unsigned average(unsigned a, unsigned b)
{
    return (a & b) + (a ^ b) / 2;
}

If your compiler supports integers larger than the size of an unsigned, say because unsigned is a 32-bit value but the native register size is 64-bit, or because the compiler supports multiword arithmetic, then you can cast to the larger data type:
unsigned average(unsigned a, unsigned b)
{
    // Suppose "unsigned" is a 32-bit type and
    // "unsigned long long" is a 64-bit type.
    return ((unsigned long long)a + b) / 2;
}


code snipped from talk: CppCon 2019: Marshall Clow “std::midpoint? How Hard Could it Be?”
constexpr Integer midpoint(Integer a, Integer b) noexcept {
	using U = std::make_unsigned_t<Integer>;

	int sign = 1;
	U m = a;
	U M = b;

	if (a > b) {  // no branch generated.
		sign = -1;
		m = b;
		M = a;
	}

	return a + sign * Integer(U(M-m) >> 1); // C casting works while we are sure input is integral types.
}


template<typename T>
constexpr enable_if_t<is_pointer_v<T>, T>
midpoint_ptr(T a, T b) {
	return a + midpoint(ptrdiff_t{0}, b - a); // std::ptrdiff_t is the signed integer type of the result of subtracting two pointers.
}

template<typename T>
constexpr enable_if_t<is_floating_point_v<Fp>, Fp>
midpoint_fp(T a, T b) noexcept {
	Fp lo = numeric_limits<Fp>::min()*2;
	Fp hi = numeric_limits<Fp>::max()*2;

	return abs(a) <= hi && abs(b) <= hi ? // typical case
		(a + b) / 2: // alwas correctly rounded
		abs(a) < lo ? a + b/2: // not safe to halve a
		abs(b) < lo ? a/2 + b: // not safe to halve b
			a/2 + b/2; // otherwise correctly rounded
}

Nov 14, 2018

[Cppcon 2016] High Performance Code 201: Hybrid Data Structures - Chandler Carruth


std::vector's problem, no SSO(small size optimization)
Why? Because STD says when move a vector, it's iterator can't be invalidated.
That implies std::vector's iterator as pointer points to heap memory.
However, SSO vector when moves, it copies, and invalidates the iterators.

Domain specific data structure has it's purpose of efficiency.
(Less corner cases, easier to design.)

Besides domain specific data structure,
why not just using customized allocator with std::vector?

Thus SSO for vector becomes:
template<typename T, int N>
using SmallVector = std::vector<T, short_alloc<T, N>>;

void fun(){
    SmallVector<int, 4>::allocator_type::arena_type a;
    SmallVector<int> v{a};
}

But...
It doesn't work well with interface boundary,
i.e  due to std::vector has allocator as type argument.
void jump(SmallVector<int, 4> &v);

void fun(){
    // Taking short_alloc<int, 8>
    SmallVector<int, 8>::allocator_type::arena_type a;
    SmallVector<int> v{a};
    jump(v);  // doesn't work... callee taking short_alloc<int, 4>
}

Another issue, callee's return type could reference to memory on callee's stack..
i.e
SmallVector<int, 4> fun(){
    SmallVector<int, 4>::allocator_type::arena_type a;
    SmallVector<int> v{a};
    return v; // BAD
}

All of all, the SmallVector loses it's value semantics.
Which is IMPORTANT.

With Domain Specific Type, these issues solved.
SmallVector type in Clang:
template <typename T, unsigned N>
class SmallVector : public SmallVectorImpl<T> {
  typedef typename SmallVectorImpl<T>::U U; // expected-error {{no type named 'U' in 'SmallVectorImpl<CallSite>'}}
  enum {

    MinUs = (static_cast<unsigned int>(sizeof(T))*N + // expected-error {{invalid application of 'sizeof' to an incomplete type 'CallSite'}}
             static_cast<unsigned int>(sizeof(U)) - 1) /
            static_cast<unsigned int>(sizeof(U)),
    NumInlineEltsElts = MinUs
  };
  U InlineElts[NumInlineEltsElts];
public:
  SmallVector() : SmallVectorImpl<T>(NumInlineEltsElts) {
  }

};

Small-size optimization is best when the values are small.
(in C++, copy by value is the default mechanism, although not being well/widely known...)

Design:
  1. Give large objects address identity.
    i.e Use object's memory address as identity avoids object's content equality test.
  2. SmallVector<std::unique_ptr<BigObject>, 4> Objects;
    
    BumpPtrAllocator impl, purpose, make BigObject as compact as possible on heap memory.
    // FAST
    class BumpPtrAllocator {
        constexpr int SlabSize = 4096;
        SmallVector<void *, 4> Slabs;
        void *CurPtr, *End;
    
    public:
        void *Allocate(int Size) P
            if (Size >= (End - CurPtr)) {
                CurPtr = malloc(SlabSize);
                End = CurPtr + SlabSize;
                Slabs.push_back(CurPtr);
            }
    
            void *Ptr = CurPtr;
            CurPtr += Size;
            return Ptr;
        }
        // ...
    };
    
  3. If pointers are too large, use an index.
  4. Aggressively pack the bits.
    PointerIntPair:
    http://llvm.org/doxygen/classllvm_1_1PointerIntPair.html
    PointerEmbeddedInt:
    http://llvm.org/doxygen/classllvm_1_1PointerEmbeddedInt.html
    TinyPtrVector:
    http://llvm.org/doxygen/classllvm_1_1TinyPtrVector.html
    Thus, we have SmallMutiMap:
  5. template<typename KeyT, typename ValueT>
    using SmallMultiMap = SmallDenseMap<KeyT, TinyPtrVector<ValueT>>;
    
  6. Use bitfields everywhere.
  7. Sometimes, we need an ordering.
    i.e comparison operator.
  8. Where possible, sort the vector.
    i.e gives you a linear BST, works well with CPU pre-fetching.
  9. What if there's no intrinsic ordering?
    We have SmallSetVector:
    (Has a set, has a vector, when insert, check data in set, and insert into vector.)
    http://llvm.org/doxygen/classllvm_1_1SmallSetVector.html

Nov 2, 2018

[cppcon 2018] OOP Is Dead, Long Live Data-oriented Design - Stoyan Nikolov


Data-Oiented Design

OOP marries data with operations

  • Heterogeneous data is brought together by a 'logical' black box object.
  • The object is used in vastly different contexts
  • Hides 'state' all over the place
  • Impact on
    • Performance
    • Scalability
    • Modifiability
    • Testability
  • Why? Cache miss~

Data-oriented design

  • Like Golang, data first
  • Separates data from logic
  • Structs and functions live independent lives
  • Data is regarded as information that has to be transformed
  • The logic embraces the data
  • Does not try to hide the logic
  • Leads to functions that work on arrays
  • Reorganizes data according to it's usage

If we aren't going to use a piece of information, why packs it together?

Examples from Chromium code base :-)

--
class CORE_EXPORT Animation final: public ~
--


So, for OOP in Chromium:
  • Uses more than 6 non-trivial classes
  • Objects contain smart pointers to other objects
  • Interpolation uses abstract classes to handle different property types
  • CSS Animations directly 'reach out' to other systems - coupling
  • Calling events
  • Setting values in DOM element
  • What's the lifetime of elements being synchronized?



DOD:
  • Data operations
    • Tick -> 99.9%
    • Add
    • Remove
    • Pause
    • ...
  • Tick Input
    • Definition
    • Time
  • Tick Output
    • Changed properties
    • New property values
    • Who owns the new values
  • Design for 'many animations',
    i.e many objects


Define a type:
struct AnimationController{
    AnimationState* as_ [];
};

// Golang style.
// No shared_ptr, every instance of this type
// has it's own value. 
// Thread safe.
struct AnimationState{
    AnimationID Id;
    time StartTime;
    time PauseTime;
    ...
};

// Avoid type erasure, use template
template<typename T>
struct AnimationStateProperty : public AnimationState {
    AnimatedDefiniationFrames<T> Keyframes;
};


// We can't use vector<baseType>
// But since we know every property types,
// create vector for each type
CSSVector<AnimationStateProperty<ZIndex>> m_ZIndexActiveAnimState;

// Iterates them for every CSSVector types

With above design, keep in mind,
std::vector
is the best container to avoid cache misses!
(continuous memory, sequential container)



Avoid branches:
  • Keep lists per-boolean 'flag'
  • Separate Active and Inactive animations
    i.e Base on the states we have, put object into a list of the same state.
  • avoid using 'if branch' test.
  • Avoid 'if (isActive)'
  • If there are too many states, try to cut down the size of states, or put the state that changes most into 'list' style.



Add API to the caller:
  • We don't have OOP style object, thus
    no member functions!
    i.e Animation.Play()
  • Use free function taking ID!
    i.e
    void PlayAnimation(AnimationID aid);


Key points:
  • Keep data flat (Golang style)
    • Maximise cache usage
    • No RTTI
    • Amortized dynamic allocations
    • Some read-only duplication improves performance and readability
  • Existence-based predication
    • Reduce branching
    • Apply the same operation on a whole table
  • Id-Based handles
    • No pointers
    • Allow rearranging internal memory
  • Table-based output
    • No external dependencies
    • Easy to reason about the flow


Scalability:
  • OOP multi-threading
    • Complicated
  • DoD multi-threading
    • Group state into list
    • Each task/job/thread keeps a private table of modified data
    • Join merges the tables (thread.join)
    • Classic fork-join


Testability:
  • OOP case
    • Hard to mock(lots of types)
    • Hidden states
    • Asserting correct state is difficult - multiple output points(VERY BAD DESIGN)
  • DOD case
    • Contract style design
    • Easier to mock(less types)
    • Asserting correct state is easy

    
Modifiability:
  • OOP
    • Hard to modify base types
    • But, easy to do 'quick' changes, because we have if branches
  • DOD
    • FP style. Building blocks
    • A bit harder to to quick changes, but with FP, we have monoid.

    
Downsides of DOD:
  • Correct data separation can be hard
    • Know the problem well
  • Existence-based predication is not always feasible(or easy)
  • 'Quick' modifications can be tough


What to keep from OOP:
  • Simple struct with simple methods are fine
  • Keep polymorphism & interface under control
  • Use template
  • Use 'impl'


Extra reference:

Jul 4, 2018

[algorithm][data structure] KD Tree

[algorithm][data structure] Quad Tree

// C++ Implementation of Quad Tree
#include <iostream>
#include <cmath>
using namespace std;
 
// Used to hold details of a point
struct Point
{
    int x;
    int y;
    Point(int _x, int _y)
    {
        x = _x;
        y = _y;
    }
    Point()
    {
        x = 0;
        y = 0;
    }
};
 
// The objects that we want stored in the quadtree
struct Node
{
    Point pos;
    int data;
    Node(Point _pos, int _data)
    {
        pos = _pos;
        data = _data;
    }
    Node()
    {
        data = 0;
    }
};
 
// The main quadtree class
class Quad
{
    // Hold details of the boundary of this node
    Point topLeft;
    Point botRight;
 
    // Contains details of node
    Node *n;
 
    // Children of this tree
    Quad *topLeftTree;
    Quad *topRightTree;
    Quad *botLeftTree;
    Quad *botRightTree;
 
public:
    Quad()
    {
        topLeft = Point(0, 0);
        botRight = Point(0, 0);
        n = NULL;
        topLeftTree  = NULL;
        topRightTree = NULL;
        botLeftTree  = NULL;
        botRightTree = NULL;
    }
    Quad(Point topL, Point botR)
    {
        n = NULL;
        topLeftTree  = NULL;
        topRightTree = NULL;
        botLeftTree  = NULL;
        botRightTree = NULL;
        topLeft = topL;
        botRight = botR;
    }
    void insert(Node*);
    Node* search(Point);
    bool inBoundary(Point);
};
 
// Insert a node into the quadtree
void Quad::insert(Node *node)
{
    if (node == NULL)
        return;
 
    // Current quad cannot contain it
    if (!inBoundary(node->pos))
        return;
 
    // We are at a quad of unit area
    // We cannot subdivide this quad further
    if (abs(topLeft.x - botRight.x) <= 1 &&
        abs(topLeft.y - botRight.y) <= 1)
    {
        if (n == NULL)
            n = node;
        return;
    }
 
    if ((topLeft.x + botRight.x) / 2 >= node->pos.x)
    {
        // Indicates topLeftTree
        if ((topLeft.y + botRight.y) / 2 >= node->pos.y)
        {
            if (topLeftTree == NULL)
                topLeftTree = new Quad(
                    Point(topLeft.x, topLeft.y),
                    Point((topLeft.x + botRight.x) / 2,
                        (topLeft.y + botRight.y) / 2));
            topLeftTree->insert(node);
        }
 
        // Indicates botLeftTree
        else
        {
            if (botLeftTree == NULL)
                botLeftTree = new Quad(
                    Point(topLeft.x,
                        (topLeft.y + botRight.y) / 2),
                    Point((topLeft.x + botRight.x) / 2,
                        botRight.y));
            botLeftTree->insert(node);
        }
    }
    else
    {
        // Indicates topRightTree
        if ((topLeft.y + botRight.y) / 2 >= node->pos.y)
        {
            if (topRightTree == NULL)
                topRightTree = new Quad(
                    Point((topLeft.x + botRight.x) / 2,
                        topLeft.y),
                    Point(botRight.x,
                        (topLeft.y + botRight.y) / 2));
            topRightTree->insert(node);
        }
 
        // Indicates botRightTree
        else
        {
            if (botRightTree == NULL)
                botRightTree = new Quad(
                    Point((topLeft.x + botRight.x) / 2,
                        (topLeft.y + botRight.y) / 2),
                    Point(botRight.x, botRight.y));
            botRightTree->insert(node);
        }
    }
}
 
// Find a node in a quadtree
Node* Quad::search(Point p)
{
    // Current quad cannot contain it
    if (!inBoundary(p))
        return NULL;
 
    // We are at a quad of unit length
    // We cannot subdivide this quad further
    if (n != NULL)
        return n;
 
    if ((topLeft.x + botRight.x) / 2 >= p.x)
    {
        // Indicates topLeftTree
        if ((topLeft.y + botRight.y) / 2 >= p.y)
        {
            if (topLeftTree == NULL)
                return NULL;
            return topLeftTree->search(p);
        }
 
        // Indicates botLeftTree
        else
        {
            if (botLeftTree == NULL)
                return NULL;
            return botLeftTree->search(p);
        }
    }
    else
    {
        // Indicates topRightTree
        if ((topLeft.y + botRight.y) / 2 >= p.y)
        {
            if (topRightTree == NULL)
                return NULL;
            return topRightTree->search(p);
        }
 
        // Indicates botRightTree
        else
        {
            if (botRightTree == NULL)
                return NULL;
            return botRightTree->search(p);
        }
    }
};
 
// Check if current quadtree contains the point
bool Quad::inBoundary(Point p)
{
    return (p.x >= topLeft.x &&
        p.x <= botRight.x &&
        p.y >= topLeft.y &&
        p.y <= botRight.y);
}
 
// Driver program
int main()
{
    Quad center(Point(0, 0), Point(8, 8));
    Node a(Point(1, 1), 1);
    Node b(Point(2, 5), 2);
    Node c(Point(7, 6), 3);
    center.insert(&a);
    center.insert(&b);
    center.insert(&c);
    cout << "Node a: " <<
        center.search(Point(1, 1))->data << "\n";
    cout << "Node b: " <<
        center.search(Point(2, 5))->data << "\n";
    cout << "Node c: " <<
        center.search(Point(7, 6))->data << "\n";
    cout << "Non-existing node: "
        << center.search(Point(5, 5));
    return 0;
}

May 19, 2018

[algorithm][dp][leetcode] Dynamic programming thinking

Reference:
[algorithm][DP] thinking steps
Erik Demaine - Dynamic Programming I: Fibonacci, Shortest Paths

Reading note for:
Nikola Otasevic - Dynamic Programming – 7 Steps to Solve any DP Interview Problem

  1. Spot if it's a DP problem
    1. Breaking it down into a collection of simpler subproblems.
    2. Solving each of those subproblems just once,
    3. and storing their solutions.

    The next time the same subproblem occurs, instead of recomputing its solution, one simply looks up the previously computed solution.

    This saves computation time at the expense of a modest expenditure in storage space.
    Whether the problem solution can be expressed as a function of solutions to similar smaller problems.
  2. Identify problem variables
    Established that there is some recursive structure between our subproblems.

    Need to express the problem in terms of the function parameters and see which of those parameters are changing.

    Typically in interviews, you will have one or two changing parameters, but technically this could be any number.
     i.e Question: Compute edit distance between strings

    Counting the number of changing parameters is valuable to determine the number of subproblems we have to solve
    , but it is also important in its own right in helping us strengthen the understanding of the recurrence relation from step 1.
  3. Clearly express the recurrence relation
    Don't rush into implementation.

    Expressing the recurrence relation as clearly as possible will strengthen your problem understanding and make everything else significantly easier.

    Once figuring out that the recurrence relation exists and specify the problems in terms of parameters, this should come as a natural step.

    Think:
    How do problems relate to each other? In other words, let's assume that you have computed the subproblems. How would you compute the main problem?
  4. Identify the base cases
    (重要!)
    A base case is a subproblem that doesn't depend on any other subproblem.
    (Like FP's ending point/ C++ variadic template)

    In order to find such subproblems,
    1. try a few examples, see how problem simplifies into smaller subproblems,
    2. and at what point it cannot be simplified further.

    The reason a problem cannot be simplified further is that one of the parameters would become a value that is not possible given 'constraints' of a problem.

    It can be a little challenging to convert assertions that we make about parameters into programmable base cases.

    This is because, in addition to listing the assertions if want to make code look concise and not check for unnecessary conditions, we need to also think about which of these conditions are even possible.
  5. Decide if you want to implement it iteratively or recursively
    i.e (stack overflow consideration, tail call)

    In both approaches, you would have to determine the recurrence relation and the base cases.

    trade-offs:



    Stack overflow issues are typically a deal breaker and a reason why would not want to have recursion in a (backend) production system.

    However, for the purposes of the interview, as long as mentioning the trade-offs, we should typically be fine with either of the implementations.
    Should feel comfortable implementing both.
  6. Add memoization
    Used for storing the results of expensive function calls and returning the cached result when the same inputs occur again.

    Why are we adding memoization to our recursion? We encounter the same subproblems which without memoization are computed repeatedly.
    Those repetitions very often lead to exponential time complexities.

    In recursive solutions, adding memoization should feel straightforward.
    Remember that memoization is just a cache of the function results. There are times when you want to deviate from this definition in order to squeeze out some minor optimizations, but treating memoization as a function result cache is the most intuitive way to implement it.

    i.e
    1. Store function result into your memory before every return statement

    2. Look up the memory for the function result before start doing any other computation
  7. Determine Time complexity
    Count the number of states
        This will depend on the number of changing parameters in the problem

    Think about the work done per each state.
         i.e if everything else but one state has been computed, how much work do we have to do to compute that last state

May 6, 2018

[algorithm][DP] thinking steps


  1. Define subproblems : count number of subproblems
  2. Guess(part of solution)  (fib doesn't need to guess, it's specific)
  3. Relate subproblems solutions(with recursion) (想出其中的關聯)
  4. Recurse & memoize OR buttom up approach(for loop). (must be asyclic, DAG, thus can use toposort.)
  5. Solve original problem (確定真的解決原本的問題)

Supplement article: Tail call