```
class Solution(object):
def validTree(self, n, edges):
"""
:type n: int
:type edges: List[List[int]]
:rtype: bool
"""
if len(edges) != n - 1:
return False
parent = range(n)
def union(i, j):
parent[i] = j
def find(i):
if parent[i] != i:
return find(parent[i])
return i
for start, end in edges:
x = find(start)
y = find(end)
if x == y:
return False
union(x, y)
partition = set()
for i in xrange(n):
partition.add(find(i))
if len(partition) > 1:
return False
return True
```

My solution gets MLE at the last testcase (2000 nodes). I actually only used a length-n list to do Union Find. It's less memory cost compared to adjacency list IMO.