hieunm3538
Senior Member
Đăng nhập leetcode, đổi UI về old version là được thím nhéLàm thế nào giải đc trên đt vậy các thím?
Đăng nhập leetcode, đổi UI về old version là được thím nhéLàm thế nào giải đc trên đt vậy các thím?
class Solution:
def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
parents = [i for i in range(n)]
count = [1] * n
def find(parents, u):
if parents[u] != u:
parents[u] = find(parents, parents[u])
return parents[u]
def dsu(parents, count, u, v):
p_u = find(parents, u)
p_v = find(parents, v)
if p_u == p_v:
return 1
if p_u < p_v:
parents[p_v] = p_u
count[p_u] += count[p_v]
count[p_v] = 0
else:
parents[p_u] = p_v
count[p_v] += count[p_u]
count[p_u] = 0
return 0
removed_edges_count = 0
for edge in edges:
[t, u, v] = edge
if t == 3:
removed_edges_count += dsu(parents, count, u-1, v-1)
parents_A = copy.deepcopy(parents)
parents_B = copy.deepcopy(parents)
count_A = copy.deepcopy(count)
count_B = copy.deepcopy(count)
for edge in edges:
[t, u, v] = edge
if t == 1:
removed_edges_count += dsu(parents_A, count_A, u-1, v-1)
elif t == 2:
removed_edges_count += dsu(parents_B, count_B, u-1, v-1)
if count_A[0] != n or count_B[0] != n:
return -1
return removed_edges_count
Thím cho e hỏi phát recursion thì sao tính là O(1) SC được tím nhỉ? Vì bộ nhớ của stack space sẽ tăng theo n nên SC đúng của recursion implementation phải là O(n) chứ nhỉ?Cách cài đặt này không thỏa mãn ràng buộc O(1) space, ngoài ra còn quá phức tạp (sử dụng fancy functions không cần thiết).
Bài này thì tư tưởng tail recursion quá rõ ràng nên một cách khác đơn giản hơn như sau:
Chỉ cần thêm memory space cho đúng một biến bool (là p), nên SC = O(1). Cách cài đặt trên có thể chuyển thành single-line function bằng cách dùng List.fold, như sau:Code:let oddEvenList xs = let rec pick (os, es) xs p = match xs, p with | x :: xs', true -> pick (x :: os, es) xs' false | x :: xs', false -> pick (os, x :: es) xs' true | _ -> es @ os |> List.rev pick ([], []) xs true
Code:let oddEventList<'t> = List.fold (fun (os, es, p) x -> if p then (x :: os, es, not p) else (os, x :: es, not p)) ([], [], true) >> (fun (a, b, _) -> b @ a |> List.rev)
Cài đặt của mình sử dụng tail recursion nên tất cả các lời gọi đều sử dụng chung stack frame.Thím cho e hỏi phát recursion thì sao tính là O(1) SC được tím nhỉ? Vì bộ nhớ của stack space sẽ tăng theo n nên SC đúng của recursion implementation phải là O(n) chứ nhỉ?![]()
nhìn có vẻ như là tail recursion, và MS nói rằng F# optimize để không có stack growth:Thím cho e hỏi phát recursion thì sao tính là O(1) SC được tím nhỉ? Vì bộ nhớ của stack space sẽ tăng theo n nên SC đúng của recursion implementation phải là O(n) chứ nhỉ?![]()
The difference is that incountDown1
the returned value of the recursive call was used to construct the final value of the function by adding 1 to it. In contrast, incountDown2
the returned value of the recursive call is also the value of the function itself. Meaning, the recursive call is the last instruction in the function definition – a style known as tail recursion. This style allowed the compiler to transform the function into a loop, thus eliminating the need to create new stack frames.
class EdgeComparator implements Comparator<int[]> {
@Override
public int compare(int[] a, int[] b) {
if (a[0] == b[0]) {
if (a[1] == b[1]) {
return a[2] - b[2];
}
return a[1] - b[1];
}
return - (a[0] - b[0]);
}
}
class Solution {
public static int TYPE = 0;
public static int U = 1;
public static int V = 2;
public static int ALICE = 1;
public static int BOB = 2;
public static int BOTH = 3;
public int maxNumEdgesToRemove(int n, int[][] edges) {
List<int[]> bobGraph = getEdgesByType(BOB, edges);
List<int[]> aliceGraph = getEdgesByType(ALICE, edges);
List<int[]> bobSPT = getSPTFromGraph(bobGraph, n);
List<int[]> aliceSPT = getSPTFromGraph(aliceGraph, n);
if (bobSPT == null || aliceSPT == null) return -1;
Set<int[]> distincEdge = new TreeSet<>(new EdgeComparator());
distincEdge.addAll(bobSPT);
distincEdge.addAll(aliceSPT);
System.out.println(distincEdge);
return edges.length - distincEdge.size();
}
public List<int[]> getEdgesByType(int type, int[][] edges) {
List<int[]> graph = new ArrayList<>();
for (int[] edge : edges) {
if (type == edge[TYPE] || BOTH == edge[TYPE]) {
graph.add(edge);
}
}
Collections.sort(graph, new EdgeComparator());
return graph;
}
int[] parent;
public List<int[]> getSPTFromGraph(List<int[]> edges, int n) {
List<int[]> sptEdge = new ArrayList<>();
parent = new int[n + 1];
for(int i = 1 ; i <= n ; ++i) makeSet(-i);
for (int[] edge : edges) {
int u = Math.abs(findSet(edge[U]));
int v = Math.abs(findSet(edge[V]));
if(u != v) {
parent[u] = v;
sptEdge.add(new int[]{edge[TYPE], edge[U], edge[V]});
}
}
Set<Integer> hashSet = new HashSet<>();
for (int i = 1; i <= n; i++) {
hashSet.add(findSet(i));
}
if (hashSet.size() != 1) {
return null;
}
return sptEdge;
}
void makeSet(int u){
u = Math.abs(u);
parent[u] = u;
}
int findSet(int u){
u = Math.abs(u);
if(u == parent[u]) return u;
return parent[u] = findSet(parent[u]);
}
}
Cài đặt của mình sử dụng tail recursion nên tất cả các lời gọi đều sử dụng chung stack frame.
Thanks 2 bác, kiến thức mới này được được e tiếp thunhìn có vẻ như là tail recursion, và MS nói rằng F# optimize để không có stack growth:
Nhân tiện ở đây, ngoài cài đặt sử dụng tail recursion đã trình bày ở post trước, mình trình bày một cài đặt sử dụng kỹ thuật continuation rất hay gặp trong functional programming:Code:// Lists in F# are implemented as singly linked lists, // which means that operations that access only // the head of the list are O(1), and element access is O(n). // https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/lists let oddEvenList (list: int list) = list |> List.indexed |> List.partition (fun (i, _) -> i % 2 = 0) |> fun (oddList, evenList) -> let _, oddList = oddList |> List.unzip let _, evenList = evenList |> List.unzip oddList @ evenList // Test with #time directive [1..10000] |> oddEvenList Real: 00:00:00.002, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0 val it: int list = [1; 3; 5; 7; 9; 11; 13; 15; 17; 19; 21; 23; 25; 27; 29; 31; 33; 35; 37; 39; 41; 43; 45; 47; 49; 51; 53; 55; 57; 59; 61; 63; 65; 67; 69; 71; 73; 75; 77; 79; 81; 83; 85; 87; 89; 91; 93; 95; 97; 99; 101; 103; 105; 107; 109; 111; 113; 115; 117; 119; 121; 123; 125; 127; 129; 131; 133; 135; 137; 139; 141; 143; 145; 147; 149; 151; 153; 155; 157; 159; 161; 163; 165; 167; 169; 171; 173; 175; 177; 179; 181; 183; 185; 187; 189; 191; 193; 195; 197; 199; ...]
let oddEventList xs =
let rec pickcps xs k =
match xs with
| x :: x' :: xs'' -> pickcps xs'' (fun (os, es) -> k (x :: os, x' :: es))
| x :: _ -> pickcps [] (fun (os, es) -> k (x :: os, es))
| _ -> ([], []) |> k |> (fun (a, b) -> a @ b)
pickcps xs id
public class Solution
{
public int MaxNumEdgesToRemove(int n, int[][] edges)
{
UnionFind alice = new(n);
UnionFind bob = new(n);
for (int i = 0; i < edges.Length; i++)
{
int[] edge = edges[i];
if (edge[0] == 3)
{
alice.Union(edge[1], edge[2]);
bob.Union(edge[1], edge[2]);
}
}
int result = alice.redundant;
alice.redundant = 0;
bob.redundant = 0;
for (int i = 0; i < edges.Length; i++)
{
int[] edge = edges[i];
int type = edge[0];
int u = edge[1];
int v = edge[2];
if (type == 1)
{
alice.Union(u, v);
}
if (type == 2)
{
bob.Union(u, v);
}
}
if (alice.GetComponentCount() != 1 || bob.GetComponentCount() != 1)
{
return -1;
}
return result + alice.redundant + bob.redundant;
}
public class UnionFind
{
int[] parent;
public int redundant = 0;
public int GetComponentCount()
{
int componentCount = 1;
for (int i = 1; i < parent.Length; i++)
{
int parent = Find(i);
componentCount += parent == 1 ? 0 : 1;
}
return componentCount;
}
public UnionFind(int n)
{
parent = new int[n + 1];
for (int i = 1; i <= n; i++)
{
parent[i] = i;
}
}
public int Find(int a)
{
if (parent[a] == a)
{
return parent[a];
}
int result = Find(parent[a]);
parent[a] = result;
return result;
}
public void Union(int a, int b)
{
int aParent = Find(a);
int bParent = Find(b);
if (aParent == bParent)
{
redundant++;
return;
}
if (aParent < bParent)
{
parent[bParent] = aParent;
return;
}
parent[aParent] = bParent;
}
}
}
goto
, btw, no biggie Đầu bài yêu cầu loại đi tối đa các cạnh nhưng vẫn đảm bảo tính liên thông của hai đồ thị.bài hôm nay có phần gần giống với việc dùng thuật toán Kruskal tìm minimum spanning tree, nhưng làm việc trên unweighted graph nên không cần phải sort theo edge weight nữa
hiểu đại khái vậy nhưng vẫn chưa rõ lắm tại sao cách dùng type 3 edge trước rồi add edge có type khác vào sao lại là optimal, có cao nhân nào chỉ giáo không
intuition mình thấy, nhưng giờ muốn chứng minh là nó optimal thì làm ntn?Đầu bài yêu cầu loại đi tối đa các cạnh nhưng vẫn đảm bảo tính liên thông của hai đồ thị.
Ta có thể hiểu là tìm số cạnh ít nhất để cho 2 đồ thị liên thông.
Cạnh loại 3 là cạnh chung của hai đồ thị, do đó ta sẽ muốn ưu tiên giữ lại cạnh loại 3.
class Solution:
def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
# step1: use type3 edges as much as possible to connect vertices for both Bob and Alice
# step2: use type1, type2 to connect Alice vertices, Bob vertices respectively
aliceUnions = UnionFind(n + 1)
bobUnions = UnionFind(n + 1)
usedEdges = 0
for _, u, v in filter(lambda edge: edge[0] == 3, edges):
if aliceUnions.find(u) != aliceUnions.find(v):
usedEdges += 1
aliceUnions.union(u, v)
bobUnions.union(u, v)
for edgeType, u, v in filter(lambda edge: edge[0] != 3, edges):
targetUnions = aliceUnions if edgeType == 1 else bobUnions
if targetUnions.find(u) != targetUnions.find(v):
usedEdges += 1
targetUnions.union(u, v)
if not aliceUnions.getUnionCount() == bobUnions.getUnionCount() == 2:
return -1
return len(edges) - usedEdges
class UnionFind:
def __init__(self, dataSize):
self.par = list(range(dataSize))
self.size = [1] * dataSize
self.unionCount = dataSize
def find(self, u):
if self.par[u] != u:
self.par[u] = self.find(self.par[u])
return self.par[u]
def getSize(self, u):
return self.size[self.find(u)]
def union(self, u, v):
u, v = self.find(u), self.find(v)
if u == v:
return
if self.size[u] < self.size[v]:
u, v = v, u
self.par[v] = u
self.size[u] += self.size[v]
self.unionCount -= 1
def getUnionCount(self):
return self.unionCount
Cho đồ thị G. Gọi E là tập hợp tất cả các cạnh sao cho G liên thông và tổng số cạnh bé nhất. Chứng minh rằng mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.intuition mình thấy, nhưng giờ muốn chứng minh là nó optimal thì làm ntn?
giờ cho graph chỉ chứa edge type 3 và graph này là hình tam giác thì cây E có chứa toàn bộ edge trong tập B không?Cho đồ thị G. Gọi E là tập hợp tất cả các cạnh sao cho G liên thông và tổng số cạnh bé nhất. Chứng minh rằng mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
Không mất tính tổng quát, giả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3. Gọi số cạnh là nghiệm không chứa V là E0.
Mệnh đề A: Pick cạnh loại 3 không phải là phương án tối ưu để E nhỏ nhất => E = E0 + 2 (pick loại 1, 2 để nối tới V).
Tuy nhiên tồn tại phương án pick cạnh loại 3: E0 + 1. Vì E0 + 1 < E0 + 2 nên mệnh đề A sai.
=> pick cạnh loại 3 là phương án tối ưu ở mỗi vertex
=> mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
Mới sửa lại rồigiờ cho graph chỉ chứa edge type 3 và graph này là hình tam giác thì cây E có chứa toàn bộ edge trong tập B không?
à edit rồi, mà saoCho đồ thị G. Gọi E là tập hợp tất cả các cạnh sao cho G liên thông và tổng số cạnh bé nhất. Chứng minh rằng mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
Không mất tính tổng quát, giả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3. Gọi số cạnh là nghiệm không chứa V là E0.
Mệnh đề A: Pick cạnh loại 3 không phải là phương án tối ưu để E nhỏ nhất => E = E0 + 2 (pick loại 1, 2 để nối tới V).
Tuy nhiên tồn tại phương án pick cạnh loại 3: E0 + 1. Vì E0 + 1 < E0 + 2 nên mệnh đề A sai.
=> pick cạnh loại 3 là phương án tối ưu ở mỗi vertex
=> mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
thì lại không mất tính tổng quát nhỉgiả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3
Vì mỗi Vertex trên G đều liên thông, nên trường hợp chỉ chứa 1 và 2 hoặc chỉ chứa 3 thì không bàn vì không dùng để chứng minh cho mệnh đề được.à edit rồi, mà sao
thì lại không mất tính tổng quát nhỉ
không, cứ cho là ở đây chứng minh được chuyện ưu tiên type 3 edge đúng trong trường hợp có vertex có nhiều edge thuộc cả ba type (gọi là case 1 đi), thì vẫn còn phần còn lại là đi chứng minh là khi không có vertex như vầy thì logic của algo vẫn đúng, tức là khi chỉ có vertex nối edge type 1, 2, v/v, chỉ khi nào cách chứng minh những trường hợp còn lại đó dùng logic giống như case 1 thì mới dùng "không mất tính tổng quát được"Vì mỗi Vertex trên G đều liên thông, nên trường hợp chỉ chứa 1 và 2 hoặc chỉ chứa 3 thì không bàn vì không dùng để chứng minh cho mệnh đề được.
Vì đang chứng minh rằng ưu tiên pick 3, đối với các vertex chứa nhiều 1, 2, 3 thì cũng chỉ cần pick một cặp 1,2 hoặc một 3 để liên thông nên dùng vertex chứa 1, 2, 3 để đại diện vẫn được. Dù số lượng mỗi loại có là bao nhiêu cũng không mất tính tổng quát.
Không, vì G liên thông nên trường hợp chỉ có (1,2) hoặc (3) thì kệ nó nhé. Đối với vertex có chứa cả 3 loại dù số lượng tụi nó có bao nhiêu đi nữa thì cũng chỉ pick một {(1,2) OR (3)} để E0 nối với V. Nên dùng (1,2,3) là đủ để đại diện cho toàn bộ case rồi còn gì nữa.không, cứ cho là ở đây chứng minh được chuyện ưu tiên type 3 edge đúng trong trường hợp có vertex có nhiều edge thuộc cả ba type (gọi lag case 1 đi), thì vẫn còn phần còn lại là đi chứng minh là khi không có vertex như vầy thì logic của algo vẫn đúng, tức là khi chỉ có vertex nối edge type 1, 2, v/v, chỉ khi nào cách chứng minh những trường hợp còn lại đó dùng logic giống như case 1 thì mới dùng "không mất tính tổng quát được"
giờ lấy trường hợp chỉ có vertex nối edge type 1, 2 thì chứng minh vầy đâu còn đúng nhỉ![]()