HoshinoTented
2019-07-27 10:51:53
这篇文章可能需要之前没有介绍过的前置知识,在本文中会粗略介绍
纯函数式的 Haskell 在 OI 中时常会遇到一些问题
这篇文章将带你愉快地在 OI 中使用 Haskell
当然是仅限练习,考场上可是没有 Haskell 的
一些基础内容可以看 洛谷日报的 #188 期
首先讲解对数值的操作
比如 +-*/
首先是 (+) (-) (*)
> :i Num
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
-- ...
它们都被定义在了 Num
类型类里
> 1 + 2
3
> 1 - 2
-1
> 1 * 2
2
接下来是 (/)
但是如果我们看一下 (/)
的定义。。。
class Num a => Fractional a where
(/) :: a -> a -> a
...
-- Defined in ‘GHC.Real’
infixl 7 /
需要 Fractional
类型类,我们再看看 Fractional
类型类的定义
> :i Fractional
class Num a => Fractional a where
(/) :: a -> a -> a
-- ...
instance Fractional Float -- Defined in ‘GHC.Float’
instance Fractional Double -- Defined in ‘GHC.Float’
只有 Float
和 Double
实现了 Fractional
类型类
这就意味着,我们不能对 Int
使用 (/)
函数了!
但我们可以使用 div
> :i div
class (Real a, Enum a) => Integral a where
div :: a -> a -> a
infixl 7 `div`
> :i Integral
class (Real a, Enum a) => Integral a where
div :: a -> a -> a
instance Integral Int -- Defined in ‘GHC.Real’
于是可以这样进行除操作
> div 5 2
2
或者将 div
函数放在中间
> 5 `div` 2
2
最后是取模
> :t mod
mod :: Integral a => a -> a -> a
> 3 `mod` 2
1
Haskell 并没有提供 (%)
运算符,不过你可以自己定义
> (%) = mod
> 3 % 2
1
位运算也是 OI 中很重要的知识
Haskell 的位运算函数都包含在了 Data.Bits 包中
import Data.Bits
使用 (.&.) (.|.)
和 xor
来进行 且 或 异或 操作
> 2 .&. 1
0
> 2 .|. 1
3
> 2 `xor` 1
3
使用 shiftL
和 shiftR
来进行位移
> 1 `shiftL` 9
512
> 512 `shiftR` 9
1
你甚至可以定义一个 (<<)
运算符
> (<<) = shiftL
> 1 << 9
512
不过要注意的是,(>>)
被 Monad 使用了,定义 (>>)
运算符之前记得先隐藏 Prelude 中的 (>>)
运算符
Haskell 内置的列表就是一个链表实现
> :i []
data [] a = [] | a : [a]
可以通过空列表的构造器 []
和 连接列表的构造器 (:)
来构造一个列表
> 1:2:3:4:5:[]
[1,2,3,4,5]
当然也可以直接 [1, 2, 3, 4, 5]
> [1, 2, 3, 4, 5]
[1,2,3,4,5]
通过 [begin..end]
来构造一个区间
> [1..10]
[1,2,3,4,5,6,7,8,9,10]
构造一个等差序列
> [1, 2..10]
[1,2,3,4,5,6,7,8,9,10]
> [1, 3..10]
[1,3,5,7,9]
使用列表生成器
> [x | x <- [1..5], even x]
[2,4]
> [y | y <- [1..5], even y]
[2,4]
> [x + y | x <- [1..5], y <- [1..5], even x && even y]
[4,6,6,8]
这里将介绍部分列表常用函数(同时也是对 洛谷日报的 #188 期 的补充)
名称(及函数签名) | 模块 | 作用 |
---|---|---|
head :: [a] -> a | Prelude | 取出列表的首元素 |
last :: [a] -> a | Prelude | 取出列表的尾元素 |
init :: [a] -> [a] | Prelude | 取出除了最后一个元素以外的元素 |
tail :: [a] -> [a] | Prelude | 取出除了第一个元素以外的元素 |
take :: Int -> [a] -> [a] | Prelude | 获取列表前 n 个元素 |
drop :: Int -> [a] -> [a] | Prelude | 删除列表前 n 个元素 |
(++) :: [a] -> [a] -> [a] | Prelude | 连接两个列表 |
(!!) :: [a] -> Int -> a | Prelude | 随机访问 |
elem :: (Foldable t, Eq a) => a -> t a -> Bool | Prelude | 检查目标元素是否存在于列表中 |
length :: Foldable t => t a -> Int | Prelude | 返回列表长度 |
map :: (a -> b) -> [a] -> [b] | Prelude | 对列表的每个元素应用函数,并返回函数结果的列表 |
filter :: (a -> Bool) -> [a] -> [a] | Prelude | 过滤列表中元素 |
reverse :: [a] -> [a] | Prelude | 翻转列表 |
sort :: Ord a => [a] -> [a] | Data.List | 对列表排序(从小到大) |
sortBy :: (a -> a -> Ordering) -> [a] -> [a] | Data.List | 根据传入的比较函数对列表排序 |
Haskell 是纯函数式语言,但 OI 中经常涉及读入输出操作,这些都是不纯的,应该怎么办呢
Haskell 给出了解决方案:IO Monad
要理解 IO Monad
,首先就要知道什么是 Monad
那什么是 Monad
呢?
让我们看看 Monad
的定义:
> :i Monad
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
{-# MINIMAL (>>=) #-}
-- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
最重要的是 (>>=)
和 return
return
用于接收一个值,并把这个值包装进 Monad
里
(>>=)
用于取出值,并把值应用到传入的函数,然后返回一个新的 Monad
但无论是 return
还是 (>>=)
它们都不能把值从 Monad
中取出并返回
这也就意味着,如果一个值进入了 Monad
,就再也无法通过 Monad
提供的操作出来了。。。
而 Monad
的这个特性,恰好也是 IO Monad
的本质
一旦函数中使用了 IO Monad
,那么这个函数的返回值也一定会带有 IO Monad
所以也就能区分纯函数和不纯函数了
(>>=)
读作 bind
bind 是 Monad 中最重要的操作,Monad 定义中的 {-# MINIMAL (>>=) #-}
代表了实现 Monad 至少要实现 (>>=)
这个函数
我们首先看看 (>>=)
的函数签名
> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
它意为 接收一个 Monad
,进行一次运算,然后再返回一个 Monad
但要如何使用 (>>=)
呢
让我们先看看 putStrLn
(用于输出字符串,下面会提到)
> :i putStrLn
putStrLn :: String -> IO ()
它接收一个 String
,然后返回一个 IO ()
不过 IO
这个类型实现了 Monad
我们这样调用 putStrLn
> putStrLn "qwq"
qwq
但我们想要在一条语句里调用两次 putStrLn
要怎么做呢?
答案是: 使用 (>>=)
首先,我们先分别调用两次 putStrLn
> putStrLn "one"
one
> putStrLn "two"
two
然后使用 (>>=)
把这两个操作连接起来
> putStrLn "one" >>= \_ -> putStrLn "two"
one
two
或者直接使用 (>>)
> putStrLn "one" >> putStrLn "two"
one
two
还可以调用更多次的 putStrLn
> :{
| printN :: Int -> String -> IO ()
| printN 0 _ = return ()
| printN n str = putStrLn str >> printN (n - 1) str
| :}
> printN 5 "qwq"
qwq
qwq
qwq
qwq
qwq
Monad 的 (>>=)
可以用来进行连续的计算
也使得 纯函数式的 Haskell 可以以命令式的方式来编写程序
Haskell 还为 (>>=)
提供了一个语法糖: do
比如:
main :: IO ()
main = getLine >>= \s ->
putStrLn s
等价于
main :: IO ()
main = do
s <- getLine
putStrLn s
这样就显得更加命令式了
IO Monad
的部分极为重要,无法掌握这部分知识代表着无法让 Haskell 程序与系统交互,从而也无法解决题目
你可以使用这两个函数来输出: putStrLn
和 print
> putStrLn "qwq"
qwq
> print "qwq"
"qwq"
会发现两者的输出不太一样,但如果我们看看 print 的实现
会发现它其实是:
print x = putStrLn (show x)
或者
print = putStrLn . show
这两者是等价的
首先通过 show
函数把值转化为 String
,再使用 putStrLn
输出
可以使用 getChar
或者 getLine
来读入一个字符或者一整行
> getChar
1
'1'
> getLine
qwq
"qwq"
使用 words
来根据空格分割
> words <$> getLine
1 2 3
["1","2","3"]
使用 read
将字符串转化为 Int
> map read . words <$> getLine :: IO [Int]
1 2 3
[1, 2, 3]
Haskell 自带的 String
效率低下,这使得我们需要使用更高效的 String
,即 Data.Text
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
Data.Text
是 text 本体
Data.Text.IO
提供了关于 Text
的 IO 操作
比如
> map (read . T.unpack) . T.words <$> TIO.getLine :: IO [Int]
1 2 3
[1, 2, 3]
使用 read
来转换数据实在是太慢了,这使得我们需要自己写一个转换器
-- Parse Int :: Count -> Source -> Result
parseInt :: Int -> String -> Int
parseInt n [] = n
parseInt n (char:str) = parseInt (n * 10 + (ord char - ord '0')) str
main :: IO ()
main = do
xs <- map (parseInt 0 . T.unpack) . T.words <$> TIO.getLine :: IO [Int]
print xs
这样,我们就完成了 Haskell 中的读入优化
列表是链表实现,OI 中常常需要随机访问,所以列表并不胜任这份工作
虽然 Haskell 有内置的 Data.Array,但性能和易用性方面还是太差了
这里介绍 vector 库
首先导入 Data.Vector.Unboxed (Data.Vector 也是可以的,但 Unboxed 版本的 Vector 对实现了 Unbox 类型类的类型 (例如 Int) 有空间和时间优化,适合 OI 使用)
import qualified Data.Vector.Unboxed as V
什么是 qualified
?
qualified
关键字代表这个包不直接引入到全局命名空间里
而 as
则是将包重命名
比如这里重命名为 V
首先,你可以创建一个空 Vector
> :i V.empty
V.empty :: V.Unbox a => V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
或者创建只有一个元素的 Vector
> :i V.singleton
V.singleton :: V.Unbox a => a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
时间复杂度都是
创建一个带有 n 个 i 的 Vector
> :i V.replicate
V.replicate :: V.Unbox a => Int -> a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
可以像这样使用
> V.replicate 5 1 :: V.Vector Int
[1,1,1,1,1]
或者创建带有 n 个元素的 Vector
> :i V.generate
V.generate :: V.Unbox a => Int -> (Int -> a) -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
generate
接收一个 Vector
长度,和一个 (Int -> a)
的函数,意思是传入一个数组下标,返回一个数组值
最后返回一个生成好的 Vector
或者你可以选择递推的方式构造出一个 Vector
> :i V.iterateN
V.iterateN :: V.Unbox a => Int -> (a -> a) -> a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
iterateN
接收一个 Vector
长度, 一个 (a -> a)
的函数,代表接收上一个值,返回当前值
然后再接收一个初始值,最后返回构造好的 Vector
比如我们使用 iterateN
构造一个斐波那契数列
> (V.iterateN 10 (\(a, b) -> (b, a + b)) (0, 1)) :: V.Vector (Int, Int)
[(0,1),(1,1),(1,2),(2,3),(3,5),(5,8),(8,13),(13,21),(21,34),(34,55)]
发现元组的第一项是 0, 1, 1, 2, 3, 5, 8, 13 ...
这的确是斐波那契数列
你还可以把一个 Haskell 列表转化为 Vector
> :t V.fromList
V.fromList :: V.Unbox a => [a] -> V.Vector a
> V.fromList [1, 1, 4, 5, 1, 4] :: V.Vector Int
[1,1,4,5,1,4]
这些函数的时间复杂度是
我们导入 Data.Vector.Unboxed 中的访问运算符,可以简化我们的代码
import Data.Vector.Unboxed ((!))
然后就可以使用这种方式来随机访问 Vector
了
> :i (!)
(!) :: V.Unbox a => V.Vector a -> Int -> a
-- Defined in ‘Data.Vector.Unboxed’
> v = V.generate 5 id
> v ! 3
3
时间复杂度是
解决问题的时候,避免不了往 Vector
中添加值
使用 cons
把 值 添加在 Vector
的前端
> :i V.cons
V.cons :: V.Unbox a => a -> V.Vector a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
> V.cons 1 (V.fromList [2, 3, 4]) :: V.Vector Int
[1,2,3,4]
或者使用 snoc
(这不就是把 cons
反过来写了吗!) 把 值 添加在 Vector
后端
Prelude V Data.Vector.Unboxed> :i V.snoc
V.snoc :: V.Unbox a => V.Vector a -> a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
> V.snoc (V.fromList [1, 2, 3]) 4 :: V.Vector Int
[1,2,3,4]
时间复杂度都是
使用 (++)
来连接两个 Vector
不过使用 (++)
之前还需要隐藏 Haskell 自带的 (++)
import Prelude hiding ((++))
然后导入 Vector
的 (++)
import Data.Vector.Unboxed ((++))
然后连接两个 Vector
> :i (++)
(++) :: V.Unbox a => V.Vector a -> V.Vector a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
infixr 5 ++
> V.fromList [1, 2, 3] ++ V.fromList [4, 5, 6] :: V.Vector Int
[1,2,3,4,5,6]
时间复杂度是
不能被修改的 Vector
不是好 Vector
!
可以通过 (//)
运算符来实现修改值
> import Data.Vector.Unboxed ((//))
> :i (//)
(//) :: V.Unbox a => V.Vector a -> [(Int, a)] -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
接收一个被修改的 Vector
,和一个 (下标, 新值)
的列表,最后返回一个更新完的 Vector
不过要注意的是,原本的 Vector
并不会被修改
所以复杂度是 Vector
的长度,
> V.fromList [1, 2, 3, 4] // [(1, 5)] :: V.Vector Int
[1,5,3,4]
不可变 Vector
有很多缺点,比如在 oi 中十分致命的性能问题
在频繁修改的时候会产生大量拷贝,使得我们的代码 TLE
因此就要选择 OIer 们最熟悉的 可变 Vector
但可变 Vector
因为是可变的,所以就变得不纯了
这里又要请到 IO Monad
了
首先导入 Data.Vector.Unboxed 的 Mutable 版本
import qualified Data.Vector.Unboxed.Mutable as MV
import Data.Vector.Unboxed.Mutable (IOVector)
可变 Vector
的构造方式就没有 不可变 Vector
那么多样了,只提供了 new
和 repilicate
> :i MV.new
MV.new ::
(Control.Monad.Primitive.PrimMonad m, MV.Unbox a) =>
Int -> m (MV.MVector (Control.Monad.Primitive.PrimState m) a)
-- Defined in ‘Data.Vector.Unboxed.Mutable’
函数签名有些复杂,其实就是接收一个长度 Vector
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
return ()
可变 Vector
可以使用 read
函数来进行随机访问
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
v <- MV.read vec 0 -- 读取 vec 中下标为 0 的元素
print v
return ()
输出了 0
时间复杂度是
可变 Vector
可以使用 write
函数来进行随机写入
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
MV.write vec 0 1 -- 对 vec 中下标为 0 的元素赋值 1
v <- MV.read vec 0 -- 读取 vec 中下标为 0 的元素
print v
return ()
输出了 1
时间复杂度是
可变 Vector
可以使用 modify
函数来进行修改
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
MV.modify vec (+1) 0 -- 对 vec 中下标为 0 的元素应用 (+1) 函数
v <- MV.read vec 0 -- 读取 vec 中下标为 0 的元素
print v
return ()
输出了 1
时间复杂度是
如果大小不够用了,可以使用 grow
函数来扩大 Vector
的容量
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
print $ MV.length vec
vec <- MV.grow vec 1 -- 把 vec 扩大 1,然后返回
print $ MV.length vec
return ()
输出 5 和 6
不!!!没有变量我要死了。。。
可能很多 OIer 都会这样子
虽然 Haskell 中没有变量,但提供了一个数据结构
可以实现类似变量的操作
当然,依然使用了 IO Monad
首先导入 Data.IORef
包
import Data.IORef
使用 newIORef
来创建一个变量
-- newIORef :: a -> IO (IORef a)
main :: IO ()
main = do
v <- newIORef 1
return ()
使用 readIORef
来读取变量中的值
-- readIORef :: IORef a -> IO a
main :: IO ()
main = do
v <- newIORef 1
readIORef v >>= print
return ()
使用 writeIORef
来向变量中写入值
-- writeIORef :: IORef a -> a -> IO ()
main :: IO ()
main = do
v <- newIORef 1
writeIORef v 2
readIORef v >>= print
return ()
使用 modifyIORef
来修改变量中的值
相当于 readIORef
和 writeIORef
的结合
-- modifyIORef :: IORef a -> (a -> a) -> IO ()
main :: IO ()
main = do
v <- newIORef 1
modifyIORef v (+1)
readIORef v >>= print
return ()
虽然 Haskell 提供了 IORef
,但我并不支持滥用 IORef
的行为 (可以无视我说的话)
映射表也是在 OI 中常用的一种数据结构
映射表被包含在 container 库 中,在 Data.Map 包下
由于部分函数命名冲突,需要使用 qualified
import qualified Data.Map as M
可以使用 empty
来构造一个空表
singleton
来构造包含一个映射的表
fromList
从 (键, 值)
映射的列表中构造映射表
> M.empty
fromList []
> M.singleton 1 2
fromlist [(1,2)]
> M.fromList [(1, 2)]
fromlist [(1,2)]
可以使用 insert
函数来添加映射
> :i M.insert
M.insert :: Ord k => k -> a -> M.Map k a -> M.Map k a
-- Defined in ‘Data.Map.Internal’
> M.insert 1 2 M.empty
fromList [(1,2)]
使用 (!)
或者 (!?)
来访问映射表
> import Data.Map ((!), (!?))
> :i (!) (!?)
(!) :: Ord k => M.Map k a -> k -> a
-- Defined in ‘Data.Map.Internal’
(!?) :: Ord k => M.Map k a -> k -> Maybe a
-- Defined in ‘Data.Map.Internal’
> M.empty ! 1
*** Exception: Map.!: given key is not an element in the map
CallStack (from HasCallStack):
error, called at libraries\\containers\\Data\\Map\\Internal.hs:610:17 in containers-0.6.0.1:Data.Map.Internal
> M.empty !? 1
Nothing
使用 (!)
访问映射表,如果映射不存在就会抛出一个错误
而 (!?)
则是返回一个 Maybe
类型
栈可以使用 Haskell 的原生列表实现
pop :: [a] -> ([a], a)
pop (x:xs) = (xs, x)
push :: a -> [a] -> [a]
push = (:)
top :: [a] -> a
top = head
还可以使用 State Monad 来更优雅地实现,但此处不过多介绍
队列可以使用两个栈的方法来模拟,这里提供我自己实现的队列
-- Queue 输出端 输入端
data Queue a = Queue [a] [a]
instance (Show a) => Show (Queue a) where
show (Queue o i) = show $ o ++ reverse i
front :: Queue a -> a
front (Queue [] xs) = last xs
front (Queue xs _) = head xs
push :: Queue a -> a -> Queue a
push (Queue o i) v = Queue o (v:i)
pop :: Queue a -> Queue a
pop (Queue [] []) = error "empty queue"
pop (Queue [] xs) = pop $ Queue (reverse xs) []
pop (Queue (_:xs) ys) = Queue xs ys
fromList :: [a] -> Queue a
fromList xs = Queue xs []
Haskell 是惰性的,但惰性求值会影响尾递归的优化
所以我们需要添加一个扩展,使得 Haskell 严格求值
在文件开头添加这样一句话:
{-# LANGUAGE Strict #-}
Haskell 也有 O2 优化,在文件开头添加这样一句话:
{-# OPTIONS_GHC -O2 #-}
Hoogle 是 Haskell 生涯中很重要的工具
Hoogle 是 Haskell 官方的函数文档
你可以在这里面查找函数的签名和用法,包括源代码实现
或者查找各种各样的依赖库
如果对文章内容有疑惑,或者想喷我菜的,都可以在评论区留言,我会尽可能回复
参考: 并查集讨论