Основная функциональность, необходимая для простой реализации принципа включения-исключения, заключается в создании всех подмножеств k-элементов набора индексов.Используя списки, это простая рекурсия:
pick :: Int -> [a] -> [[a]]
pick 0 _ = [[]] -- There is exactly one 0-element subset of any set
pick _ [] = [] -- No way to pick any nonzero number of elements from an empty set
pick k (x:xs) = map (x:) (pick (k-1) xs) ++ pick k xs
-- There are two groups of k-element subsets of a set containing x,
-- those that contain x and those that do not
Если pick
не является локальной функцией, вызовы которой находятся на 100% под вашим контролем, вы должны добавить проверку, что параметр Int
никогда не бывает отрицательным(вы можете использовать Word
для этого параметра, тогда он встроен в тип).Если k
является длинным, проверка по длине списка для выбора предотвращает много бесплодных рекурсий, поэтому лучше встроить это с самого начала:
pick :: Int -> [a] -> [[a]]
pick k xs = choose k (length xs) xs
choose :: Int -> Int -> [a] -> [[a]]
choose 0 _ _ = [[]]
choose k l xs
| l < k = [] -- we want to choose more than we have
| l == k = [xs] -- we want exactly as many as we have
| otherwise = case xs of
[] -> error "This ought to be impossible, l == length xs should hold"
(y:ys) -> map (y:) (choose (k-1) (l-1) ys) ++ choose k (l-1) ys
тогда формула включения-исключениястановится
inclusionExclusion indices
= sum . zipWith (*) (cycle [1,-1]) $
[sum (map count $ pick k indices) | k <- [1 .. length indices]]
, где count list
подсчитывает количество элементов пересечения [subset i | i <- list]
.Конечно, вам нужен эффективный способ для вычисления этого, иначе было бы более эффективно определить размер объединения напрямую.
Существует много возможностей для оптимизации, и есть разные способы сделать это, но этодовольно короткий и прямой перевод принципа.