2025年9月30日 星期二
當數學遇上程式:用質因數分解優雅解決「三角和」問題
2025年9月29日 星期一
凸多邊形的三角形分割以及二維陣列與整數最大值的使用
今天練習的題目是 1039. Minimum Score Triangulation of Polygon,第一次遇到這種 DP 類型,後來想通後,發現解題辦法不外乎兩種。
一種是由上而下,透過記憶跟遞迴來處理,效率較差,寫成 C++ 會像是這樣:
class Solution {
public:
int minScoreTriangulation(vector<int>& values) {
int n = values.size();
array<array<int, 50>, 50> memo{};
function<int(int, int)> dp = [&](int i, int j) -> int {
if (i + 2 > j)
return 0;
if (!memo[i][j]) {
if (i + 2 == j)
return memo[i][j] =
values[i] * values[i + 1] * values[i + 2];
int score = numeric_limits<int>::max();
int product = values[i] * values[j];
for (int k = i + 1; k < j; ++k)
score =
min(score, product * values[k] + dp(i, k) + dp(k, j));
memo[i][j] = score;
}
return memo[i][j];
};
return dp(0, n - 1);
}
};
另一種是由下而上,透過疊代來處理,效率較好,寫成 C++ 會像是這樣:
class Solution {
public:
int minScoreTriangulation(vector<int>& values) {
int n = values.size();
array<array<int, 50>, 50> dp{};
for (int d = 2; d < n; ++d) {
for (int i = 0; i + d < n; ++i) {
int j = i + d;
int score = numeric_limits<int>::max();
int product = values[i] * values[j];
for (int k = i + 1; k < j; ++k)
score =
min(score, product * values[k] + dp[i][k] + dp[k][j]);
dp[i][j] = score;
}
}
return dp[0][n - 1];
}
};
用疊代的方法寫成 Python3 會像這樣:
class Solution:
def minScoreTriangulation(self, values: List[int]) -> int:
n = len(values)
dp = [[0 for _ in range(n)] for _ in range(n)]
for d in range(2, n):
for i in range(n - d):
j = i + d
score = float("inf")
product = values[i] * values[j]
for k in range(i + 1, j):
score = min(score, product * values[k] + dp[i][k] + dp[k][j])
dp[i][j] = score
return dp[0][n - 1]
用疊代的方法寫成 Go 會像這樣:
func minScoreTriangulation(values []int) int {
n := len(values)
dp := make([][]int, n)
for i := range n {
dp[i] = make([]int, n)
}
for d := 2; d < n; d++ {
for i := range n - d {
j := i + d
score := math.MaxInt
product := values[i] * values[j]
for k := i + 1; k < j; k++ {
score = min(score, product*values[k]+dp[i][k]+dp[k][j])
}
dp[i][j] = score
}
}
return dp[0][n-1]
}
用疊代的方法寫成 Rust 會像這樣:
impl Solution {
pub fn min_score_triangulation(values: Vec<i32>) -> i32 {
let n = values.len();
let mut dp = vec![vec![0; n]; n];
for d in 2..n {
for i in 0..(n - d) {
let j = i + d;
let mut score = i32::MAX;
let mut product = values[i] * values[j];
for k in (i + 1)..j {
score = score.min(product*values[k]+dp[i][k]+dp[k][j]);
}
dp[i][j] = score;
}
}
dp[0][n-1]
}
}
另外值得一提的是 Rust 目前 1.90.0 還沒有支援 nested recursive function call,雖然上面沒有全部寫出來,但是 C++, Python3, Go 都有支援,如果這題要用 Rust 寫記憶跟遞迴會蠻麻煩的,反正效率也比較差,就寫個 C++ 意思意思一下就好了。
2025年9月28日 星期日
Heapify
今日練習的題目是 976. Largest Perimeter Triangle,官方的解答是直接排序後,從後面開始找出第一個合法的三角形周長就可以了,不過還有另一種解法就是利用 Heapify,雖然最糟的情況下演算法效能會跟直接排序法差不多,但是最佳狀況會比排序法好,但是現實上也可能會因為排序法使用連續的記憶體也可能會效率更好一點,所以還是要看資料本身適合哪一種。
如果是使用 Python3 的話,因為預設是 Min Heap 所以會需要使用點小技巧來使用成 Max Heap 的樣子。
class Solution:
def largestPerimeter(self, nums: List[int]) -> int:
nums = [-num for num in nums]
heapq.heapify(nums)
a = -heapq.heappop(nums)
b = -heapq.heappop(nums)
while len(nums) > 0:
c = -heapq.heappop(nums)
if b + c > a:
return a + b + c
a, b = b, c
return 0
如果是使用 C++ 的話,由於預設是 Max Heap,所以直接倒進去 priority_queue 使用就可以了。
class Solution {
public:
int largestPerimeter(vector<int>& nums) {
priority_queue<int> pq(nums.begin(), nums.end());
int a = pq.top();
pq.pop();
int b = pq.top();
pq.pop();
while (!pq.empty()) {
int c = pq.top();
if (b + c > a)
return a + b + c;
pq.pop();
swap(a, b);
swap(b, c);
}
return 0;
}
};
如果是使用 Go 的話,要自己刻一些東西搭配 container/heap 使用,使用起來相當麻煩,不然就是要使用第三方套件才會比較輕鬆些。
import "container/heap"
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] > h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func largestPerimeter(nums []int) int {
if len(nums) < 3 {
return 0
}
h := IntHeap(nums)
heap.Init(&h)
for h.Len() >= 3 {
a := heap.Pop(&h).(int)
b := h[0]
c := h[1]
if h.Len() > 2 && h[2] > c {
c = h[2]
}
if b+c > a {
return a + b + c
}
}
return 0
}
如果是使用 Rust 的話,就需要使用到 std::collections::BinaryHeap,預設就是 Max Heap,所以使用上也算是簡單。
use std::collections::BinaryHeap;
impl Solution {
pub fn largest_perimeter(nums: Vec<i32>) -> i32 {
let mut heap = BinaryHeap::from(nums);
while heap.len() >= 3 {
let a = heap.pop().unwrap();
let b = heap.pop().unwrap();
let c = heap.pop().unwrap();
if b + c > a {
return a + b + c;
} else {
heap.push(b);
heap.push(c);
}
}
0
}
}
不過 Rust 寫成排序的方法,寫起來會比較漂亮跟簡潔,充滿著滿滿的 Rust 程式碼正統風格。
impl Solution {
pub fn largest_perimeter(mut nums: Vec<i32>) -> i32 {
nums.sort_unstable();
nums.windows(3)
.rev()
.find_map(|window| {
if window[0] + window[1] > window[2] {
Some(window[0] + window[1] + window[2])
} else {
None
}
})
.unwrap_or(0)
}
}
程式碼加速魔法:尋找最大面積三角形的三種策略
2025年9月25日 星期四
字串串接
今天練習的 LeetCode 題目是 166. Fraction to Recurring Decimal,解題的思路是使用 Hash Map 去記錄小數點後面每一位遇到的餘數的位置,如果遇到記錄過的餘數就能使用記錄的位置將括號插入,但是這個題目也是在考驗如何用程式語言來串接字串。
如果是使用 Python3 來寫的話最簡單,不用考慮整數溢位的問題, str 的串接也相當直覺容易。
class Solution:
def fractionToDecimal(self, numerator: int, denominator: int) -> str:
if numerator == 0:
return "0"
num = ""
if (numerator < 0) != (denominator < 0):
num += "-"
n = abs(numerator)
d = abs(denominator)
num += str(n // d)
r = n % d
if r == 0:
return num
num += "."
pos = {}
while r != 0:
if r in pos:
num = num[0 : pos[r]] + "(" + num[pos[r] :]
num += ")"
return num
pos[r] = len(num)
r *= 10
num += str(r // d)
r %= d
return num
如果是用 C++ 的話,就要小心整數溢位的問題,不過 string 的串接也還算簡單容易上手。
class Solution {
public:
string fractionToDecimal(int numerator, int denominator) {
if (numerator == 0)
return "0";
string num;
if ((numerator < 0) != (denominator < 0))
num += "-";
long long n = abs((long long)numerator);
long long d = abs((long long)denominator);
num += to_string(n / d);
long long r = n % d;
if (r == 0)
return num;
num += ".";
unordered_map<long long, int> pos;
while (r != 0) {
if (pos.contains(r)) {
num.insert(pos[r], "(");
num += ")";
break;
}
pos[r] = num.length();
r *= 10;
num += to_string(r / d);
r %= d;
}
return num;
}
};
如果是用 Go 的話,也要小心整數溢位的問題,不過 string 的串接最好使用 strings.Builder 比較有效率,要用 strconv.FormatInt 來轉換小數點後面的數字,比較有點小麻煩的是要準備 abs 函式,因為 Go 沒有提供!什麼這麼基本的東西居然沒提供!
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
func fractionToDecimal(numerator int, denominator int) string {
if numerator == 0 {
return "0"
}
var builder strings.Builder
if (numerator < 0) != (denominator < 0) {
builder.WriteString("-")
}
n := abs(int64(numerator))
d := abs(int64(denominator))
builder.WriteString(strconv.FormatInt(n/d, 10))
r := n % d
if r == 0 {
return builder.String()
}
builder.WriteString(".")
rMap := make(map[int64]int)
for r != 0 {
if pos, ok := rMap[r]; ok {
result := builder.String()
return result[:pos] + "(" + result[pos:] + ")"
}
rMap[r] = builder.Len()
r *= 10
builder.WriteString(strconv.FormatInt(r/d, 10))
r %= d
}
return builder.String()
}
如果是用 Rust 的話,就要懂得使用 String::new() 跟 push_str() 還有 push() 的方法,還有如何使用 char::from_digit() 來轉換字元,當然也需要注意整數溢位的問題。
use std::collections::HashMap;
impl Solution {
pub fn fraction_to_decimal(numerator: i32, denominator: i32) -> String {
if numerator == 0 {
return "0".to_string();
}
let mut result = String::new();
if (numerator < 0) != (denominator < 0) {
result.push('-');
}
let n = (numerator as i64).abs();
let d = (denominator as i64).abs();
result.push_str(&(n / d).to_string());
let mut remainder = n % d;
if remainder == 0 {
return result;
}
result.push('.');
let mut remainder_map = HashMap::<i64, usize>::new();
while remainder != 0 {
if let Some(&pos) = remainder_map.get(&remainder) {
result.insert(pos, '(');
result.push(')');
break;
}
remainder_map.insert(remainder, result.len());
remainder *= 10;
if let Some(digit_char) = char::from_digit((remainder / d) as u32, 10) {
result.push(digit_char);
}
remainder %= d;
}
result
}
}
最後是 Go 跟 Rust 檢查餘數是否存在於 Hash Map 中的方式,比起 C++ 跟 Python3 來說比較特別,展現了各自程式語言本身的設計哲學。
2025年9月23日 星期二
字串分割
今日的 165. Compare Version Numbers Solved 使用 Python3 五分鐘就可以解掉了,主要的思考套路是設法弄出兩個等長的整數陣列來比較就可以了。
class Solution:
def compareVersion(self, version1: str, version2: str) -> int:
ver1 = [int(x) for x in version1.split(".")]
ver2 = [int(x) for x in version2.split(".")]
for x, y in zip_longest(ver1, ver2, fillvalue=0):
if x < y:
return -1
elif x > y:
return 1
return 0
如果要用 C++ 就會有點囉唆,要使用 stringstream 搭配 getline 才行,不是那麼直覺,之後再用 resize 調整成一樣的長度來比較。
class Solution {
vector<int> parse(string version) {
string num;
stringstream ss(version);
vector<int> ver;
while (getline(ss, num, '.'))
ver.emplace_back(stoi(num));
return ver;
}
public:
int compareVersion(string version1, string version2) {
auto ver1 = parse(version1);
auto ver2 = parse(version2);
int len = max(ver1.size(), ver2.size());
ver1.resize(len, 0);
ver2.resize(len, 0);
for (int i = 0; i < len; ++i) {
if (ver1[i] < ver2[i])
return -1;
else if (ver1[i] < ver2[i])
return 1;
}
return 0;
}
};
如果是 Go 要熟悉一下內建的 strings.Split 以及 strconv.Atoi
func compareVersion(version1 string, version2 string) int {
str1 := strings.Split(version1, ".")
str2 := strings.Split(version2, ".")
length := max(len(str1), len(str2))
ver1 := make([]int, length)
ver2 := make([]int, length)
for i, s := range str1 {
num, _ := strconv.Atoi(s)
ver1[i] = num
}
for i, s := range str2 {
num, _ := strconv.Atoi(s)
ver2[i] = num
}
for i := range length {
if ver1[i] < ver2[i] {
return -1
} else if ver1[i] > ver2[i] {
return 1
}
}
return 0
}
如果是 Rust 要懂得使用 split, filter_map, collect 的串接組合技,跟 trim, parse, ok 來將字串轉換成整數,以及用 resize 將陣列調整成一樣長度,再用跟 Python 一樣的 zip 結合起來一個一個比較。
impl Solution {
pub fn compare_version(version1: String, version2: String) -> i32 {
let mut ver1: Vec<i32> = version1.split('.').filter_map(|s| s.trim().parse::<i32>().ok()).collect();
let mut ver2: Vec<i32> = version2.split('.').filter_map(|s| s.trim().parse::<i32>().ok()).collect();
let len = ver1.len().max(ver2.len());
ver1.resize(len, 0);
ver2.resize(len, 0);
for (x, y) in ver1.into_iter().zip(ver2) {
if x < y {
return -1;
} else if x > y {
return 1;
}
}
0
}
}
2025年9月17日 星期三
C++ 自製的 gcd 比標準函式庫提供的跑得還要快?
今天在用 C++ 重複練習 2197. Replace Non-Coprime Numbers in Array 一遍,試著寫出跑得更快的版本,發現自己寫出來的 gcd 在 LeetCode 平台上反而跑得比較快,目前還不曉得背後到底是怎麼一回事,先記錄一下,目前想到的做法是盡量避免呼叫 gcd 以及盡量使用已使用的記憶體空間來加速,最好的時候可以跑到 Runtime 2ms Beats 98.62% 的成績。
class Solution {
int gcd(int a, int b) {
if (b > a)
swap(a, b);
while (b != 0) {
a %= b;
swap(a, b);
}
return a;
}
public:
vector<int> replaceNonCoprimes(vector<int>& nums) {
int idx = 0;
for (int i = 1; i < nums.size(); ++i) {
int num = gcd(nums[idx], nums[i]);
if (num == 1)
nums[++idx] = nums[i];
else
nums[idx] = nums[idx] / num * nums[i];
for (int j = idx; j > 0; --j) {
int num = gcd(nums[j], nums[j - 1]);
if (num == 1)
break;
nums[j - 1] *= nums[j] / num;
--idx;
}
}
nums.resize(idx + 1);
return nums;
}
};
2025年9月16日 星期二
Rust 沒官方標準支援 gcd 跟 lcm 的 traits 蠻令人意外的
今天在練習 2197. Replace Non-Coprime Numbers in Array 時,需要使用到 GCD 跟 LCM,在 C++/Python 都有官方支援,Rust 跟 Go 都沒有支援,有點意外。
impl Solution {
pub fn replace_non_coprimes(nums: Vec<i32>) -> Vec<i32> {
let mut arr = Vec::with_capacity(nums.len());
let mut curr = nums[0];
let gcd = |mut a: i32, mut b: i32| -> i32 {
if a < b {
(a, b) = (b, a);
}
while b != 0 {
(a, b) = (b, a % b);
}
a
};
let lcm = |a: i32, b: i32| -> i32 {
a / gcd(a, b) * b
};
for &num in &nums[1..] {
if gcd(num, curr) > 1 {
curr = lcm(num, curr);
while let Some(&last) = arr.last() && gcd(curr, last) > 1 {
curr = lcm(last, curr);
arr.pop();
}
} else {
arr.push(curr);
curr = num;
}
}
arr.push(curr);
arr
}
}
這裡還剛好可以使用到 Rust 1.88 版本才開始有支援的 Let chains
,Let chains
讓程式碼的表示更直白,真是太棒了。
if let Channel::Stable(v) = release_info()
&& let Semver { major, minor, .. } = v
&& major == 1
&& minor == 88
{
println!("`let_chains` was stabilized in this version");
}