diff --git a/src/structure/collections/multi-set.ts b/src/structure/collections/multi-set.ts
index bdebe3f745c6511b78cd70719a6c790af38656fb..9319c10fae19b1075655c42101d19b374c0acb8c 100644
--- a/src/structure/collections/multi-set.ts
+++ b/src/structure/collections/multi-set.ts
@@ -90,10 +90,12 @@ namespace MultiSet {
         return subtractEE(a, b);
     }
 
-    // TODO: union
+    export function union(a: MultiSet, b: MultiSet): MultiSet {
+        return findUnion([a, b]);
+    }
 
-    export function union(sets: ArrayLike<MultiSet>): MultiSet {
-        return 0 as any;
+    export function unionMany(sets: ArrayLike<MultiSet>): MultiSet {
+        return findUnion(sets);
     }
 
     class ElementsIterator implements Iterator<IntPair> {
@@ -145,7 +147,6 @@ namespace MultiSet {
 
 const pair = IntPair.zero();
 
-
 function isArrayLike(x: any): x is ArrayLike<number> {
     return x && (typeof x.length === 'number' && (x instanceof Array || !!x.buffer));
 }
@@ -198,6 +199,28 @@ function _createObjectOrdered(keys: OrderedSet, data: { [id: number]: OrderedSet
     return ret;
 }
 
+function getUniqueElements(xs: number[]) {
+    let count = 1;
+    for (let i = 1, _i = xs.length; i < _i; i++) {
+        if (xs[i - 1] !== xs[i]) count++;
+    }
+    const ret = new (xs as any).constructor(count);
+    ret[0] = xs[0];
+    let offset = 1;
+    for (let i = 1, _i = xs.length; i < _i; i++) {
+        if (xs[i - 1] !== xs[i]) ret[offset++] = xs[i];
+    }
+    return ret;
+}
+
+function normalizeArray(xs: number[]) {
+    sortArray(xs);
+    for (let i = 1, _i = xs.length; i < _i; i++) {
+        if (xs[i - 1] === xs[i]) return getUniqueElements(xs);
+    }
+    return xs;
+}
+
 function ofPackedPairs(xs: ArrayLike<number>): MultiSet {
     if (xs.length === 0) return MultiSet.Empty;
     const sets: { [key: number]: number[] } = Object.create(null);
@@ -213,7 +236,7 @@ function ofPackedPairs(xs: ArrayLike<number>): MultiSet {
     for (const _k of Object.keys(sets)) {
         const k = +_k;
         keys[keys.length] = k;
-        ret[k] = OrderedSet.ofSortedArray(sortArray(sets[k]));
+        ret[k] = OrderedSet.ofSortedArray(normalizeArray(sets[k]));
     }
     return ofObject1(keys, ret);
 }
@@ -339,4 +362,62 @@ function subtractEE(a: MultiSetElements, b: MultiSetElements) {
     return ofObjectOrdered(OrderedSet.ofSortedArray(keys), ret);
 }
 
+function findUnion(sets: ArrayLike<MultiSet>) {
+    if (!sets.length) return MultiSet.Empty;
+    if (sets.length === 1) return sets[0];
+    if (sets.length === 2 && sets[0] === sets[1]) return sets[0];
+
+    const eCount = { count: 0 };
+    const ns = unionN(sets, eCount);
+    if (!eCount.count) return ns;
+    const ret = Object.create(null);
+    for (let i = 0, _i = sets.length; i < _i; i++) {
+        const s = sets[i];
+        if (typeof s !== 'number') unionInto(ret, s);
+    }
+    if (MultiSet.size(ns) > 0) {
+        if (typeof ns === 'number') unionIntoN(ret, ns);
+        else unionInto(ret, ns);
+    }
+    return ofObject(ret);
+}
+
+function unionN(sets: ArrayLike<MultiSet>, eCount: { count: number }) {
+    let countN = 0, countE = 0;
+    for (let i = 0, _i = sets.length; i < _i; i++) {
+        if (typeof sets[i] === 'number') countN++;
+        else countE++;
+    }
+    eCount.count = countE;
+    if (!countN) return MultiSet.Empty;
+    if (countN === sets.length) return ofPackedPairs(sets as ArrayLike<number>);
+    const packed = new Float64Array(countN);
+    let offset = 0;
+    for (let i = 0, _i = sets.length; i < _i; i++) {
+        const s = sets[i];
+        if (typeof s === 'number') packed[offset++] = s;
+    }
+    return ofPackedPairs(packed);
+}
+
+function unionInto(data: { [key: number]: OrderedSet }, a: MultiSetElements) {
+    const keys = a.keys;
+    for (let i = 0, _i = keys.size; i < _i; i++) {
+        const k = keys.elementAt(i);
+        const set = data[k];
+        if (set) data[k] = OrderedSet.union(set, a[k]);
+        else data[k] = a[k];
+    }
+}
+
+function unionIntoN(data: { [key: number]: OrderedSet }, a: number) {
+    IntPair.unpack(a, pair);
+    const set = data[pair.fst];
+    if (set) {
+        data[pair.fst] = OrderedSet.union(set, OrderedSet.ofSingleton(pair.snd));
+    } else {
+        data[pair.fst] = OrderedSet.ofSingleton(pair.snd);
+    }
+}
+
 export default MultiSet
\ No newline at end of file
diff --git a/src/structure/spec/collections.spec.ts b/src/structure/spec/collections.spec.ts
index 4bf0092baa02ea31d96e30347e59b1225649ff0d..1ad4858020498e97eb88446d1538e63133b50bf9 100644
--- a/src/structure/spec/collections.spec.ts
+++ b/src/structure/spec/collections.spec.ts
@@ -404,4 +404,19 @@ describe('multiset', () => {
         expect(setToPairs(MultiSet.subtract(c, a))).toEqual([]);
         expect(setToPairs(MultiSet.subtract(d, a))).toEqual([p(2, 3)]);
     });
+
+    it('union', () => {
+        const a = MultiSet.create([r(1, 3), r(0, 1)]);
+        const a1 = MultiSet.create([r(1, 3), r(0, 1)]);
+        const b = MultiSet.create([r(10, 3), r(0, 1)]);
+        const c = MultiSet.create([r(1, 3)]);
+        const d = MultiSet.create([r(2, 3)]);
+        expect(MultiSet.unionMany([a])).toBe(a);
+        expect(MultiSet.union(a, a)).toBe(a);
+        expect(setToPairs(MultiSet.union(a, a))).toEqual([p(0, 1), p(1, 3)]);
+        expect(setToPairs(MultiSet.union(a, a1))).toEqual([p(0, 1), p(1, 3)]);
+        expect(setToPairs(MultiSet.union(a, b))).toEqual([p(0, 1), p(1, 3), p(10, 3)]);
+        expect(setToPairs(MultiSet.union(c, d))).toEqual([p(1, 3), p(2, 3)]);
+        expect(setToPairs(MultiSet.unionMany([a, b, c, d]))).toEqual([p(0, 1), p(1, 3), p(2, 3), p(10, 3)]);
+    });
 });
\ No newline at end of file