-
-
Notifications
You must be signed in to change notification settings - Fork 50.4k
Feat/bit next higher same ones #13103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
0xPrashanthSec
wants to merge
13
commits into
TheAlgorithms:master
from
0xPrashanthSec:feat/bit-next-higher-same-ones
Closed
Changes from 9 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4ca35cc
feat(ciphers): add scytale (skytale) transposition cipher with doctests
a27583a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] c6e3d9f
chore(ciphers): satisfy ruff UP006/UP035 by using builtin generics
2b2e584
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] e018e65
style(ciphers): fix import block formatting (isort I001)
16ebe36
feat(ciphers): add columnar transposition cipher with doctests
5ea264a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 369788d
refactor(ciphers): improve variable naming for clarity in columnar tr…
3aac7a0
feat(bit_manipulation): add next_higher_same_ones (SNOOB) with doctests
5b93e2b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 14dee00
Update next_higher_same_ones.py
0xPrashanthSec dee6b71
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 5d0634e
Merge branch 'master' into feat/bit-next-higher-same-ones
0xPrashanthSec File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| """Next higher integer with the same number of set bits (SNOOB). | ||
| author: @0xPrashanthSec | ||
|
|
||
| Given a non-negative integer n, return the next higher integer that has the same | ||
| number of 1 bits in its binary representation. If no such number exists within | ||
| Python's unbounded int range (practically always exists unless n is 0 or all | ||
| ones packed at the most significant end for fixed-width), this implementation | ||
| returns -1. | ||
|
|
||
| This is the classic SNOOB algorithm from "Hacker's Delight". | ||
|
|
||
| Reference: https://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation | ||
|
|
||
| >>> next_higher_same_ones(0b0011) | ||
| 5 | ||
| >>> bin(next_higher_same_ones(0b0011)) | ||
| '0b101' | ||
| >>> bin(next_higher_same_ones(0b01101)) # 13 -> 14 (0b01110) | ||
| '0b1110' | ||
| >>> next_higher_same_ones(1) | ||
| 2 | ||
| >>> next_higher_same_ones(0) # no higher with same popcount | ||
| -1 | ||
| >>> next_higher_same_ones(-5) # negative not allowed | ||
| Traceback (most recent call last): | ||
| ... | ||
| ValueError: n must be a non-negative integer | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
|
|
||
| def next_higher_same_ones(n: int) -> int: | ||
| """Return the next higher integer with the same number of set bits as n. | ||
|
|
||
| :param n: Non-negative integer | ||
| :return: Next higher integer with same popcount or -1 if none | ||
| :raises ValueError: if n < 0 | ||
| """ | ||
| if n < 0: | ||
| raise ValueError("n must be a non-negative integer") | ||
| if n == 0: | ||
| return -1 | ||
|
|
||
| # snoob algorithm | ||
| # c = rightmost set bit | ||
| c = n & -n | ||
| # r = ripple carry: add c to n | ||
| r = n + c | ||
| if r == 0: | ||
| return -1 | ||
| # ones = pattern of ones that moved from lower part | ||
| ones = ((r ^ n) >> 2) // c | ||
| return r | ones | ||
|
|
||
|
|
||
| if __name__ == "__main__": # pragma: no cover | ||
| import doctest | ||
|
|
||
| doctest.testmod() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """Columnar Transposition cipher. | ||
|
|
||
| This classical cipher writes the plaintext in rows under a keyword and reads | ||
| columns in the order of the alphabetical rank of the keyword letters. | ||
|
|
||
| Reference: https://en.wikipedia.org/wiki/Transposition_cipher#Columnar_transposition | ||
|
|
||
| We keep spaces and punctuation. Key must be alphabetic (case-insensitive). | ||
|
|
||
| >>> pt = "WE ARE DISCOVERED. FLEE AT ONCE" | ||
| >>> ct = encrypt(pt, "ZEBRAS") | ||
| >>> decrypt(ct, "ZEBRAS") == pt | ||
| True | ||
|
|
||
| Edge cases: | ||
| >>> encrypt("HELLO", "A") | ||
| 'HELLO' | ||
| >>> decrypt("HELLO", "A") | ||
| 'HELLO' | ||
| >>> encrypt("HELLO", "HELLO") | ||
| 'EHLLO' | ||
| >>> decrypt("EHLLO", "HELLO") | ||
| 'HELLO' | ||
| >>> encrypt("HELLO", "") | ||
| Traceback (most recent call last): | ||
| ... | ||
| ValueError: Key must be a non-empty alphabetic string | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
||
| def _normalize_key(key: str) -> str: | ||
| k = "".join(ch for ch in key.upper() if ch.isalpha()) | ||
| if not k: | ||
| raise ValueError("Key must be a non-empty alphabetic string") | ||
| return k | ||
|
|
||
|
|
||
| def _column_order(key: str) -> list[int]: | ||
| # Stable sort by character then original index to handle duplicates | ||
| indexed = list(enumerate(key)) | ||
| return [i for i, _ in sorted(indexed, key=lambda indexed_pair: (indexed_pair[1], indexed_pair[0]))] | ||
|
|
||
|
|
||
| def encrypt(plaintext: str, key: str) -> str: | ||
| """Encrypt using columnar transposition. | ||
|
|
||
| :param plaintext: Input text (any characters) | ||
| :param key: Alphabetic keyword | ||
| :return: Ciphertext | ||
| :raises ValueError: on invalid key | ||
| """ | ||
| k = _normalize_key(key) | ||
| cols = len(k) | ||
| if cols == 1: | ||
| return plaintext | ||
|
|
||
| order = _column_order(k) | ||
|
|
||
| # Build ragged rows without padding | ||
| rows = (len(plaintext) + cols - 1) // cols | ||
| grid: list[str] = [plaintext[i * cols : (i + 1) * cols] for i in range(rows)] | ||
|
|
||
| # Read columns in sorted order, skipping missing cells | ||
| out: list[str] = [] | ||
| for col in order: | ||
| for r in range(rows): | ||
| if col < len(grid[r]): | ||
| out.append(grid[r][col]) | ||
| return "".join(out) | ||
|
|
||
|
|
||
| def decrypt(ciphertext: str, key: str) -> str: | ||
| """Decrypt columnar transposition ciphertext. | ||
|
|
||
| :param ciphertext: Encrypted text | ||
| :param key: Alphabetic keyword | ||
| :return: Decrypted plaintext | ||
| :raises ValueError: on invalid key | ||
| """ | ||
| k = _normalize_key(key) | ||
| cols = len(k) | ||
| if cols == 1: | ||
| return ciphertext | ||
|
|
||
| order = _column_order(k) | ||
| text_len = len(ciphertext) | ||
| rows = (text_len + cols - 1) // cols | ||
| r = text_len % cols | ||
|
|
||
| # Column lengths based on ragged last row (no padding during encryption) | ||
| col_lengths: list[int] = [] | ||
| for c in range(cols): | ||
| if r == 0: | ||
| col_lengths.append(rows) | ||
| else: | ||
| col_lengths.append(rows if c < r else rows - 1) | ||
|
|
||
| # Slice ciphertext into columns following the sorted order | ||
| columns: list[str] = [""] * cols | ||
| idx = 0 | ||
| for col in order: | ||
| ln = col_lengths[col] | ||
| columns[col] = ciphertext[idx : idx + ln] | ||
| idx += ln | ||
|
|
||
| # Rebuild plaintext row-wise | ||
| out: list[str] = [] | ||
| pointers = [0] * cols | ||
| for _ in range(rows * cols): | ||
| c = len(out) % cols | ||
| if pointers[c] < len(columns[c]): | ||
| out.append(columns[c][pointers[c]]) | ||
| pointers[c] += 1 | ||
| return "".join(out) | ||
|
|
||
|
|
||
| if __name__ == "__main__": # pragma: no cover | ||
| import doctest | ||
|
|
||
| doctest.testmod() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| """Scytale (Skytale) transposition cipher. | ||
|
|
||
| A classical transposition cipher used in ancient Greece. The sender wraps a | ||
| strip of parchment around a rod (scytale) and writes the message along the rod. | ||
| The recipient with a rod of the same diameter can read the message. | ||
|
|
||
| Reference: https://en.wikipedia.org/wiki/Scytale | ||
|
|
||
| Functions here keep characters as-is (including spaces). The key is a positive | ||
| integer representing the circumference count (number of rows). | ||
|
|
||
| >>> encrypt("WE ARE DISCOVERED FLEE AT ONCE", 3) | ||
| 'WA SVEFETNERDCEDL C EIOR EAOE' | ||
| >>> decrypt('WA SVEFETNERDCEDL C EIOR EAOE', 3) | ||
| 'WE ARE DISCOVERED FLEE AT ONCE' | ||
|
|
||
| Edge cases: | ||
| >>> encrypt("HELLO", 1) | ||
| 'HELLO' | ||
| >>> decrypt("HELLO", 1) | ||
| 'HELLO' | ||
| >>> encrypt("HELLO", 5) # key equals length | ||
| 'HELLO' | ||
| >>> decrypt("HELLO", 5) | ||
| 'HELLO' | ||
| >>> encrypt("HELLO", 0) | ||
| Traceback (most recent call last): | ||
| ... | ||
| ValueError: Key must be a positive integer | ||
| >>> decrypt("HELLO", -2) | ||
| Traceback (most recent call last): | ||
| ... | ||
| ValueError: Key must be a positive integer | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
||
| def encrypt(plaintext: str, key: int) -> str: | ||
| """Encrypt plaintext using Scytale transposition. | ||
|
|
||
| Write characters around a rod with `key` rows, then read off by rows. | ||
|
|
||
| :param plaintext: Input message to encrypt | ||
| :param key: Positive integer number of rows | ||
| :return: Ciphertext string | ||
| :raises ValueError: if key <= 0 | ||
| """ | ||
| if key <= 0: | ||
| raise ValueError("Key must be a positive integer") | ||
| if key == 1 or len(plaintext) <= key: | ||
| return plaintext | ||
|
|
||
| # Read every key-th character starting from each row offset | ||
| return "".join(plaintext[row::key] for row in range(key)) | ||
|
|
||
|
|
||
| def decrypt(ciphertext: str, key: int) -> str: | ||
| """Decrypt Scytale ciphertext. | ||
|
|
||
| Reconstruct rows by their lengths and interleave by columns. | ||
|
|
||
| :param ciphertext: Encrypted string | ||
| :param key: Positive integer number of rows | ||
| :return: Decrypted plaintext | ||
| :raises ValueError: if key <= 0 | ||
| """ | ||
| if key <= 0: | ||
| raise ValueError("Key must be a positive integer") | ||
| if key == 1 or len(ciphertext) <= key: | ||
| return ciphertext | ||
|
|
||
| length = len(ciphertext) | ||
| base = length // key | ||
| extra = length % key | ||
|
|
||
| # Determine each row length | ||
| row_lengths: list[int] = [base + (1 if r < extra else 0) for r in range(key)] | ||
|
|
||
| # Slice ciphertext into rows | ||
| rows: list[str] = [] | ||
| idx = 0 | ||
| for r_len in row_lengths: | ||
| rows.append(ciphertext[idx : idx + r_len]) | ||
| idx += r_len | ||
|
|
||
| # Pointers to current index in each row | ||
| pointers = [0] * key | ||
|
|
||
| # Reconstruct by taking characters column-wise across rows | ||
| result_chars: list[str] = [] | ||
| for i in range(length): | ||
| r = i % key | ||
| if pointers[r] < len(rows[r]): | ||
| result_chars.append(rows[r][pointers[r]]) | ||
| pointers[r] += 1 | ||
| return "".join(result_chars) | ||
|
|
||
|
|
||
| if __name__ == "__main__": # pragma: no cover | ||
| import doctest | ||
|
|
||
| doctest.testmod() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.