标签
更新 NFLSPC #1 搜索

搜索语法速查表

2019-05-08 01:30:13 By TRCYX
搜索字符串格式
搜索字符串含义
abc在标题和用户名中搜索 abc
abc def在标题和用户名中搜索 abcdef
#123要求搜索结果的编号是 $123$
#1?3#1#3要求搜索结果的编号是以 $1$ 开头、以 $3$ 结尾的三位数
#1*要求搜索结果的编号以 $1$ 开头
#1* #*2 def要求搜索结果的编号是以 $1$ 开头或以 $2$ 结尾,并且标题或用户名包含 def
@alice在搜索博客时要求作者为 alice;或在搜索用户时要求用户名为 alice
@b*b?在搜索博客时要求作者名以 b 开头,倒数第二个字符是另一个 b
或在搜索用户时要求用户名满足相同条件
+模板题要求题目和博客有 模板题 标签
+模板题 +DP要求题目和博客有 模板题DP 标签
:p仅搜索题目
:p:a仅搜索 AC 过的题目
:p:a仅搜索未 AC 的题目
:c仅搜索比赛
:c:u仅搜索未开始的比赛
:c:r仅搜索正在进行的比赛
:c:f仅搜索已结束的比赛
:c:nf仅搜索未结束的比赛
:b仅搜索博客
:b:b仅搜索文档类型的博客
:b:s仅搜索幻灯片类型的博客
:u仅搜索用户
:c:nf :b:s :u仅搜索未结束的比赛、幻灯片和用户
abc def #1* #*2 @a* @*b? +模板题 +DP :p:a :c:nf :b:b :u要求搜索结果的标题或用户名包含 abcdef
如果有编号则以 $1$ 开头、以 $2$ 结尾
如果有相关联的用户名则要求以 a 开头、以 b 结尾
题目和博客要求有 模板题DP 标签
仅在 AC 过的题目、未结束的比赛、文档类型的博客和用户中搜索
+DP abc :u #*2 @a* #1* def @*b? :c:nf :b:b +模板题 :p:a同上
\#123在标题和用户名中搜索 #abc
\:problems在标题和用户名中搜索 :problems
\\abc在标题和用户名中搜索 \abc
\def在标题和用户名中搜索 \def
"a b" "c d"在标题和用户名中搜索 a bc d
"a \" b"在标题和用户名中搜索 a " b
"a \\ b"在标题和用户名中搜索 a \ b
"a \\\" b"在标题和用户名中搜索 a \" b
"a \other\ b"在标题和用户名中搜索 a \other\ b
"a \\\ b"在标题和用户名中搜索 a \\ b
"+a \\\\ b"要求题目和博客有 a \\ b 标签
"\\abc"在标题和用户名中搜索 \abc
"\\+abc"在标题和用户名中搜索 +abc
"\\\\+abc"在标题和用户名中搜索 \+abc
快速跳转格式
跳转字符串含义
#123跳转到编号为 $123$ 的题目
@root跳转到唯一的名为 root 的用户的个人信息
@*a?c*跳转到唯一的用户名形如 *a?c* 的用户的个人信息
:p #123跳转到编号为 $123$ 的题目
:c #2跳转到编号为 $2$ 的比赛
:b #280跳转到编号为 $280$ 的博客
:b @*a?c*跳转到唯一的用户名形如 *a?c* 的用户的博客首页
:u @*a?c*跳转到唯一的用户名形如 *a?c* 的用户的个人信息

搜索功能的更新

2019-05-08 01:28:57 By TRCYX

搜索框增强

之前增强了右上角搜索框的搜索和跳转功能。下面会介绍一些可以让你的搜索比直接搜单词更准确的方法。

  • 严格来说,这算是制造了一门小语言。考虑到用户主要是 OIer,学习能力强,我就没有封装一层用按钮、选项框等组件做的壳。(套一层壳也不复杂,欢迎有兴趣的同学来套壳)

太长不看版


基础格式

首先,搜索目前可以在题目、比赛、博客和用户中进行。

  • 搜索字符串被分成若干个词。每个形如 abc 的词表示搜索所有完整包含 abc 的内容。此处包含指题目、比赛和博客的标题中包含,或者用户的用户名中包含。例子如下:(✔ 表示会搜索到,✖ 表示不会搜索到)
搜索字符串 \ 标题或用户名abcXabcXa bcadbc
abc
  • 当有多个词时,每个词的结果会被合并,即只需要包含至少一个词就会被搜索到(类似对条件进行或):
搜索字符串 \ 标题或用户名abcXdefXdef-abca bc d efadbecf
abc defdef abcabc abc def

阅读更多……

NFLSPC #1 题解合集

2018-12-17 02:38:01 By TRCYX

NFLSPC #1 H题 Xor sum 题解

2018-12-16 01:31:46 By TRCYX

全都去掉就好了啊 →_→

一般会认为一个例如幺半群的结构上, $0$ 个元素做某种运算的结果是幺半群的单位元,比如:

  • $0$ 个实数的和是 $0$
  • $0$ 个非 $0$ 实数的积是 $1$
  • $0$ 个自然数异或的和是 $0$
  • $0$ 个多项式的卷积是 $1$
  • $0$ 个积性函数的狄利克雷卷积是 $\varepsilon$

反正这个比赛一看就不正经,交一发试试看说不定就 A 了呢。

NFLSPC #1 L题 L 题解

2018-12-16 01:12:46 By TRCYX

比赛现场没有 C++11 还是 32 位的。。有毒

首先,$\lambda x . N$ 的伪代码我感觉写得还不够清楚,这样说可能更准确且容易理解:如果 $f = \lambda x . N$,可以用伪代码表示如下:

function f(x) {
    return 将 x = (x 的实际值) 代入 N 中的结果;
}

下面我们来按测试点顺序解题。

测试点 1

$true$ 的定义已经给出,

true = \x . \y . x

只要读完题(的主要部分)就知道

false = \x . \y . y

观察题面可以注意到这样一件事: $$f~x = N \Longleftrightarrow f = \lambda x . N$$ 所以 $false$ 的推断是显然的: $$ \begin{array}{crcl} & (false~x)~y & = & y \\ \Longleftrightarrow & false~x & = & \lambda y . y \\ \Longleftrightarrow & false & = & \lambda x . (\lambda y . y) = \lambda x . \lambda y . y \end{array} $$ 下面会不加说明地使用这样的结论。

测试点 2

我们会希望有一个类似 a ? b : c 的运算符,或者说 if。这样就可以:

not x = x ? false : true
and x y = x ? (y ? true : false) : false
or x y = x ? true : (y ? true : false)

仔细观察 $true$ 和 $false$ 的定义,会发现它们本身就能达到这样的要求,即如果 $a$ 是 $true$ 或 $false$,则 a ? b : c 可以表示成 $(a~b)~c$,所以

not x = x false true
and x y = x (y true false) false
or x y = x true (y true false)

当然,$y~true~false$ 就是 y ? true : false,和 $y$ 是等价的,所以一个简洁的答案是:

not = \x . x false true
and = \x . \y . x y false
or = \x . \y . x true y
测试点 3

提示提到 $true$ 和 $false$ 有选择功能,所以能够取出 $pair~x~y$ 中的 $x$ 和 $y$,将它们当作 $true$ 或 $false$ 的参数,就可以实现 $first$ 和 $second$:

pair x y = ?
first (pair x y) = true x y
second (pair x y) = false x y

可惜 $\lambda$-演算不内置模式匹配,没法这么做。我们在 $first$ 和 $second$ 的函数体里,只能将参数 $pair~x~y$ 当作一个整体,而无法直接拆开 $x$ 和 $y$。但是提示中又说:可以让 $pair$ 接受更多参数。那我们只好把 $true$ 和 $false$ 送进 $pair~x~y$,试图让它把这两个选择器作用于 $x$ 和 $y$:

pair x y = ?
first p = p true
second p = p false

那么 $pair$ 就必须接受第三个参数:

pair x y = \f . ?

考虑 ? 是什么。$(pair~x~y)~true = true~x~y$,$(pair~x~y)~false = false~x~y$,所以可以让

pair x y = \f . f x y

pair = \x . \y . \f . f x y
first = \p . p true
second = \p . p false

这可能有点绕,我们回想一下 $first~(pair~a~b)$ 是如何计算的: $$ \begin{align} & first~(pair~a~b) \\ = & (pair~a~b)~true \\ = & (\lambda x . \lambda y . \lambda f . f~x~y)~a~b~true \\ = & (\lambda f . f~a~b)~true \\ = & true~a~b \\ = & a \end{align} $$ 注意到 $pair$ 在作用在 $a$ 和 $b$ 上后,得到的 $(\lambda f . f~a~b)$ 捕获了 $a$ 和 $b$,从而我们可以再传入 $f$,对 $a$ 和 $b$ 进行操作。在这样的基础上,我们不仅可以传入 $true$ 和 $false$,甚至可以传入任意的双参数函数,使得两个参数是 $pair$ 的两个元素。通过这样的方式,我们也能构造任意数量成员的简单元组:

triple = \x . \y . \z . \f . f x y z
first = \t . t (\x . \y . \z . x)
second = \t . t (\x . \y . \z . y)
third = \t . t (\x . \y . \z . z)

这样,我们就用函数表示了数据。我们在这里简单谈一点对“数据”这个概念的理解。

$pair~x~y$ 这个数据本身是一个函数,似乎并不是很符合直觉。不过数据本身不一定要是内存中的二进制字节,一个表达式也可以将数据编码,只要我们能通过一定方法构造这种数据($pair$),然后通过一定方法读取数据中我们需要的部分($first$ 和 $second$),并使得这种数据满足某些约定。

在这里,$pair$ 被称为构造函数,而 $first$ 和 $second$ 被称为选择器,我们可以认为数据就是由构造函数、选择器和一些数据上的约定组成的。事实上,例如在 C++ 中,make_pair 可视为 pair 的构造函数(不是 C++ 意义上的构造函数),而 .first.second 就作为选择器而存在(虽然语法上不是一个函数,但通过这种方法我们得以选择数据中的两个部分)。这个例子中,$pair$ 上并没有很明显的限制条件。通过构造函数和选择器,我们可以在数据上进行一系列操作。

这样来观察数据,我们也得以进行一层抽象,将数据的使用和数据的内部表示隔离,不同的表示可以进行相同的使用,例如如果我们这样完成这个测试点:

pair = \x . \y . \f . f y x
first = \p . p false
second = \p . p true

我们在之后的测试点使用这三个函数的时候,并不会和之前的实现方式有什么不同。类似这样的剥离接口和实现的抽象在程序设计中是非常重要的。

测试点 4

显然我们的列表会形如 $$insert~a_0~(insert~a_1~(insert~a_2~\cdots(insert~a_{n-1}~nil))),$$ 即满足 $$l \equiv insert~(head~l)~(tail~l).$$ 这和 $pair$ 的形式相当接近,所以我们会想:

insert = pair
head = first
tail = second

这样,列表成为了一棵用 $pair$ 组合而成的向右的单边树,或者说,$first$ 是值、$second$ 是指针的链表。每个节点的值是列表中该位置的值($head$),指针指向下一个节点,即后面的元素组成的子列表($tail$)。

的确,在不考虑 $isnil$ 的情况下一切都能顺利工作。如果需要支持 $isnil$,我们可能需要在节点上记录更多的值(参考上面的 $triple$):

makenode value isnil next = \f . f value isnil next
getvalue node = node (\x . \y . \z . x)
getisnil node = node (\x . \y . \z . y)
getnext node = node (\x . \y . \z . z)

然后将节点连在一起:

insert value list = makenode value false list
// 列表本身就是一个节点

head list = getvalue list
tail list = getnext list

// 只有 makenode 的第二个参数 isnil 有实际意义,其他两个 false 是随便填的
nil = makenode false true false

isnil list = getisnil list

整理一下,可以得到

makenode = \value . \isnil . \next . (\f . f value isnil next)
getvalue = \node . node (\x . \y . \z . x)
getisnil = \node . node (\x . \y . \z . y)
getnext = \node . node (\x . \y . \z . z)

insert = \x . \l . makenode x false l
head = \l . getvalue l
tail = \l . getnext l
nil = makenode false true false
isnil = \l . getisnil l

当然,可以不必把前四个函数显式地写出来;一个节点只要能包含值、是否是 $nil$、下一个节点这样三个信息,具体实现也是无所谓的,如

nil = pair true true
isnil = \p . first p
head = \l . first (second l)
tail = \l . second (second l)
insert = \x . \l . pair false (pair x l)

还有一些不用每个节点存三个信息的表示方法,但相比这样的做法可能稍微有点难理解,故在此不再赘述。

注意到这里没有要求对链表做加入首节点之外的操作,如将列表倒序,这是因为这样的处理整个列表的操作需要递归,而递归在 $\lambda$-演算中没有普通的编程语言中那么直白。在第十个测试点介绍 $\textsf{Y}$ 组合子后,我们将可以实现全列表的操作。

现在,我们可以实现一些局部的操作,如给列表换头:

changehead list newhead = insert newhead (tail list)

当然,由于一个已经构造出的表示列表的表达式不可能真的改变 $head$,所以这里事实上构造了一个新的列表,其 $tail$ 和原列表相同,而 $head$ 是新的值。这种不可以真的改变,而只能通过构造新的实例来得到改变后的结果的数据被称为“不可变数据”。不可变数据在 OI 中与可持久化是密切相关的(如函数式线段树)。

测试点 5

对于 $compose$,

(compose f g) x = f (g x)
compose f g = \x . f (g x)
compose = \f . \g . \x . f (g x)

这是显然的。通常在数学中 $compose~f~g$ 用 $f \circ g$ 表示。

对于 $pair$ 的 $map$ 也是显然的:

(map f) (pair x y) = pair (f x) (f y)
map f = \p . pair (f (first p)) (f (second p))
map = \f . \p . pair (f (first p)) (f (second p))

通常情况下 $map$ 是在列表上做的,但因为需要递归,所以在这里只要求实现一个简化版本。如果允许(直接)递归,列表上的 $map$ 可以这么实现:

map = \f . \list . insert (f (head list)) (map f (tail list))

从而用比较简洁的语法(不需要显式地循环)将函数 $f$ 作用于列表 $list$ 的所有元素。

高阶函数在函数式程序设计中是一个相当重要的概念。(在一个复杂的程序中)通过将函数作为参数传递或当作返回值返回,可以避免重复写相似的代码,将共同的部分抽象出来。C++11 允许将函数作为“第一类公民”,标准库中提供了若干常用的高阶函数,如 std::transform 和上面的 $map$ 功能类似。

(之所以这里只有两个简单的例子是因为列表操作需要递归以及高阶函数不容易测试正确性)

测试点 6

一个数字的表示是比较简单的($zero$ 和 $false$ 是一样的,这很有意思):

zero = \f . \x . x
three = \f . \x . f (f (f x)))

three = \f . compose f (compose f f)

对于 $succ$,比较 $n$ 和 $n + 1$ 的表示: $$ \begin{align} n & = \lambda f . \lambda x . \underbrace{f~(f~\cdots~(f~x))}_\text{共有 $n$ 个 $f$} \\ succ~n & = \lambda f . \lambda x . \underbrace{f~(f~\cdots~(f~x))}_\text{共有 $n + 1$ 个 $f$} \end{align} $$ 如果我们把 $\lambda f . \lambda x . $ 移到等式左边,可以得到 $$ \begin{align} n~f~x & = \underbrace{f~(f~\cdots~(f~x))}_\text{共有 $n$ 个 $f$} \\ (succ~n)~f~x & = \underbrace{f~(f~\cdots~(f~x))}_\text{共有 $n + 1$ 个 $f$} \\ & = f~(\underbrace{f~(f~\cdots~(f~x))}_\text{共有 $n$ 个 $f$}) \\ & = f~(n~f~x) \end{align} $$ 所以可以用拆开邱奇数的 $\lambda f . \lambda x . $ 的方式定义 $succ$:

succ = \n . \f . \x . f (n f x)

另一方面,我们会发现 $$n~f = \underbrace{f \circ f \circ f \circ \cdots \circ f}_\text{共有 $n$ 个 $f$} = f^{\circ n}$$ 其中 $\circ$ 表示之前的 $compose$。从这个角度上看, $$(succ~n)~f = f^{\circ (n + 1)} = f \circ f^{\circ n} = f \circ (n~f)$$ 所以

succ = \n . \f . compose f (n f)
测试点 7

从 $n~f = f^{\circ n}$ 的角度看,$plus$ 和 $mul$ 会比较显然:

  • 对于 $plus$,$f^{\circ (m + n)} = f^{\circ m} \circ f^{\circ n}$,所以有
    plus m n = \f . compose (m f) (n f)
  • 对于 $mul$,$f^{\circ (m \times n)} = (f^{\circ m})^{\circ n}$,所以有
    mul m n = \f . n (m f)

对于 $exp$, $$ \begin{align} & (exp~m~n)~f \\ = & f^{\circ m^n} \\ = & f^{\circ (\overbrace{m \times m \times \dots \times m}^\text{共有 $n$ 个 $m$})} \\ = & \underbrace{m~(m~(m~\cdots~(m~f)))}_\text{共有 $n$ 个 $m$} \end{align} $$ 我们会发现这和邱奇数的形式非常接近:邱奇数 $n$ 将 $f$ 在 $x$ 上作用了 $n$ 次,而 $exp~m~n$ 将 $m$ 在 $f$ 上作用了 $n$ 次。因此,

exp m n = n m
测试点 8

我们没有办法通过 $O(1)$ 的操作来从将 $f^{\circ n} = f \circ f^{\circ (n - 1)}$ 中将最外面的 $f$ 直接剥去。所以,可以采取一个看起来比较蠢的办法:

  • 因为我们可以用邱奇数将函数 $f$ 在 $x$ 上作用 $n$ 遍,而 $n - 1$ 与 $n$ 接近,我们可以考虑对 $0$ 进行 $n$ 次左右的 $succ$。
  • 但是,进行 $n$ 次 $succ$ 就和 $n$ 一样了,所以我们希望能特判第一次,即第一次什么都不做,之后每次对现在的数进行 $succ$。
  • 考虑平时写程序时用一个布尔变量记录“现在是不是第一次”的方法,因为我们已经有了 $true$ 和 $false$,并可以通过 $b x y$ ($b$ 是 $true$ 或 $false$) 来选择进行 $x$ 或 $y$(类似于 if),所以这是可行的。

具体来说,我们用一个 $pair$ 记录现在是否是第一次,以及被 $succ$ 的数。

init = pair true zero

我们根据这个 $pair$ 中的 $first$ 决定不动(或者返回 $0$),还是进行 $succ$:

succifnotfirst p = (first p) zero (succ (second p))

不过我们要进行 $n$ 次的操作必须也返回一个 $pair$,不然无法对其作用 $n$ 次。返回的 $pair$ 的 $first$ 是 $false$,表示已经至少进行过一次操作了:

operation p = pair false (succifnotfirst p)

这样,我们只要对 $init$ 进行 $n$ 次 $operation$:

result n = n operation init

由于返回值是一个 $pair$,我们要取出 $pair$ 的 $second$ 作为 $pred$ 的返回值:

pred n = second (result n)

整理得到

init = pair true zero
operation = \p . pair false ((first p) zero (succ (second p)))
pred = \n . second (n operation init)

当然也有其他解法,如

extract = \container . container (\u . u)
pred = \n . \f . \x . extract (n (\g . \h . h (g f)) (\u . x))

这里

  • 将 $v = f^{\circ n}(x)$ 包装成 $\lambda h . h~v$ 的容器,通过 $extract$ 提取容器中的值
  • $\lambda g . \lambda h . h~(g~f)$ 通过对容器 $g$ 里的值作用 $f$,并用 $\lambda h . h$ 重新包装成容器,从而达到类似 $succ$ 的效果
  • $\lambda u . x$ 通过这种容器的设置,起到了一个类似容器中包装 $-1$ 的效果,即 $g~f$ 中 $g = \lambda u . x$ 时,$g~f = x$,类似于 $succ$ 后为 $0~f~x$。
  • 最后从容器中 $extract$ 出 $(pred~n)~f~x$。
测试点 9

有了 $pred$ 减法就相当简单了。一次 $pred$ 能让一个数减 $1$,$n$ 次 $pred$ 就能减 $n$,所以

minus m n = n pred m
测试点 10

对于 $iszero$,我们可以用类似 $pred$ 的套路:一开始是 $true$,进行一次之后就永远是 $false$,一共进行 $n$ 次。这里我们没有必要用包装一个 $pair$,因为除了这个布尔值,我们没有什么要存的东西:

iszero n = n (\x . false) true

在之前的测试点中,有一个可能有些奇怪的规定:$pred~zero = zero$。这意味着,对于 $a \leq b$,$minus~a~b = b~pred~a = zero$。这样一来,$leq$ 就可以用 $minus$ 和 $iszero$ 实现:

leq m n = iszero (minus m n)

而 $eq$ 显然是可以用 $leq$ 实现的:如果 $a \leq b$ 且 $b \leq a$,那么必然有 $a = b$,所以

eq m n = and (leq m n) (leq n m)
测试点 11

在邱奇数的编码中,减法已经相当困难了,如果直接做除法可能难以达成。如果我们思考我们平时是怎么做除法的以及高精度除法的实现,结合关于递归的提示,可以发现除法能用递归的减法表示: $$m \div n = \textrm{if}~m < n~\textrm{then}~0~\textrm{else}~1 + (m - n) \div n$$ 用 $\lambda$-演算表示的话,就是 $$divide^\prime = \lambda m . \lambda n . (leq~(succ~m)~n)~zero~(succ~(divide^\prime~(minus~m~n)~n))$$ 这里用了 $divide^\prime$ 而不是 $divide$,是因为这并不是真的 $\lambda$-表达式的项,它的定义中引用了自身。

根据 $\textsf{Y}$ 组合子的介绍,我们要考虑将 $divide^\prime$ 变成这样: $$div = \lambda c . \lambda m . \lambda n . (leq~(succ~m)~n)~zero~(succ~(c~(minus~m~n)~n))$$ 使得本来应该递归调用自身的地方,用一个传入的参数 $c$ 代替,并祈祷 $\textsf{Y}$ 组合子将 $divide^\prime$ 当作 $c$ 传进来,从而使函数体变得和 $divide^\prime$ 等价。

幸运的是,$\textsf{Y}$ 组合子干的正是这件事。根据题面中的推导,$\textsf{Y}$ 组合子能找到 $div$ 的不动点,即 $\textsf{Y}~div \equiv div~(\textsf{Y}~div)$。观察等式可知,左侧的 $\textsf{Y}~div$ 就是右侧将 $\textsf{Y}~div$ 当作参数 $c$ 传入 $div$ 的结果,写成等式就是: $$\textsf{Y}~div = div~(\textsf{Y}~div) = \lambda m . \lambda n . (leq~(succ~m)~n)~zero~(succ~((\textsf{Y}~div)~(minus~m~n)~n))$$ 这时,将 $\textsf{Y}~div$ 视为一个整体,上式就和 $divide^\prime$ 的(不严谨)定义完全一样,每处 $divide^\prime$ 都被替换成 $\textsf{Y}~div$。$\textsf{Y}~div$ 就是我们想要的 $divide^\prime$。

这样一来,我们就可以得到一个答案:

y = \f . (\x . f (x x)) (\x . f (x x))
div = \c . \m . \n . (leq (succ m) n) zero (succ (c (minus m n) n))
divide = y div

当然,为了效率考虑,计算 $m < n$ 和 $m - n$ 事实上都进行了一次减法,减法是相当耗时间的事情。(邱奇数的运算相当慢,所以测试数据中只能用 $10$ 以内的数字进行除法。)如果能合并两次类似的减法,效率能够大幅度提升。因为 $m < n$ 用 $m + 1 \leq n$ 计算,而 $m + 1 \leq n$ 包含的减法是 $(m + 1) - n$ 而不是 $m - n$($((m + 1) - n) - 1$ 并不永远等于 $m - n$),所以似乎要将 $m + 1 \leq n$ 改成 $m \leq n$ 才能合并。我们把 $div$ 修改为 $divone$,这个函数使 $\textsf{Y}~div$ 计算 $(m - 1) \div n$,这样一来两边的减法就一样了:

divone = \c . \m . \n . (leq m n) zero (succ (c (minus m n) n))
divide = \m . \n . (y divone) (succ m) n

此时我们还没有真的合并两次减法。我们先 inline $leq~m~n$:

divone = \c . \m . \n . (iszero (minus m n)) zero (succ (c (minus m n) n))

怎么将两个相同的项合并到一起去呢?我们可能希望有一个 $\textrm{let}~a = v~\textrm{in}~B$ 的语法,表示将 $a = v$ 代入 $B$,这样一来就可以只算一次:

divone = \c . \m . \n . let d = (minus m n) in (iszero d) zero (succ (c d n))

可惜并没有这种语法。但是,参考我在一开始提到的,将 $\lambda x . N$ 解释为这样一个函数,它调用时返回将 $x$ 的值代入 $N$ 的计算结果,这个概念和我们想要的 $\textrm{let}~\dots~\textrm{in}~\dots$ 是相当类似的。事实上,我们可以这样实现 $\textrm{let}~a = v~\textrm{in}~B$: $$\textrm{let}~a = v~\textrm{in}~B \Longleftrightarrow (\lambda a . B)~v$$ 通过这样的方式,我们可以在当前上下文增加一个名字 $a$ 的绑定。所以

divone = \c . \m . \n . (\d . (iszero d) zero (succ (c d n))) (minus m n)

当然,这个题目的测试数据并没有卡常,所以这个优化在这里是无关紧要的。


以上是题目答案。

如果允许使用正常的数字等符号的话,$\lambda$-演算其实算一门表达概念还算舒服的编程语言,有不少函数式编程语言的语法和它相当类似。近年来由于计算机算力提升以及互联网行业蓬勃发展,函数式程序设计重新得到关注,许多语言都加入了 $\lambda$-表达式以及函数作为“一等公民”的支持,某种意义上可以算是来源于邱奇的 $\lambda$-演算。

我在出这道题之前和戴大爷胡诌说要出一个一道题入门函数式编程的提答,不过似乎没法把比较宏观的函数式思想迅速压入 $\lambda$-演算的语法,也不容易测试,所以就选了几个邱奇编码的实现,然后试图用提示来达到“一道题入门"的效果,不过似乎冗长的题面不是特别友好,再加上 evaluator 现场无法运行,做的人比较少,就难以有入门这一说了。(就不提选的题都太难了这种整体上的问题了。)

希望大家能从玩这道题的过程中获得快乐233

(未完成的)NFLSPC #1 G题 Encryption 题解

2018-12-11 21:58:33 By TRCYX

这题是 @fizzydavid 去年出的,不过去年不会造通讯题,所以就咕了一年。

题面中没有给样例,似乎影响了一些人做题,我们之前都没有发现,在这里给大家道歉。样例现已补上。

子任务 1

直接输出原矩阵即可。

在接下来的子任务里,我们需要在不丢失原矩阵信息的基础上(最简单的方法就是不改变原矩阵),在矩阵上增加两行两列,这两行两列需要包含一些标志,使得经过中间的变换后,解密程序能够通过找到这些标记,从而知道如何还原原矩阵。

子任务 2

两种翻转是独立的,而每种翻转进行两次之后就会和不做任何改变一样,所以事实上只有 $2 \times 2 = 4$ 种不同的变换结果,可以参考下图(虽然图中是以旋转作为基本操作的,但结果相同):

四种不同变换结果

我们先考虑上下翻转。因为矩阵可能被翻转也可能不被翻转,我们对矩阵做一些有方向性的标志。因为可以增加两行,我们可以在整个矩阵上方加上一行 $1$,整个矩阵下方加上一行 $0$: $$ \begin{array}{ccccc} \color{red}1 & \color{red}1 & \color{red}1 & \color{red}\cdots & \color{red}1 \\ a_{0, 0} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} \\ a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} \\ a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} \\ \color{blue}0 & \color{blue}0 & \color{blue}0 & \color{blue}\cdots & \color{blue}0 \end{array} $$ 这样,解密程序可以根据收到的矩阵最上面一行任意位置是 $1$ 还是 $0$ 决定是否要上下翻转矩阵。

对于左右翻转也是差不多的。我们可以在之前的基础上,在矩阵左侧加上一列 $1$,整个矩阵右侧加上一列 $0$: $$ \begin{array}{ccccccc} \color{orange}? & 1 & 1 & 1 & \cdots & 1 & \color{orange}? \\ \color{red}1 & a_{0, 0} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} & \color{blue}0 \\ \color{red}1 & a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} & \color{blue}0 \\ \color{red}1 & a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} & \color{blue}0 \\ \color{red}\vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \color{blue}\vdots \\ \color{red}1 & a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} & \color{blue}0 \\ \color{orange}? & 0 & 0 & 0 & \cdots & 0 & \color{orange}? \end{array} $$ 这样,解密程序可以根据收到的矩阵最左边一列(中间)任意位置是 $1$ 还是 $0$ 决定是否要左右翻转矩阵。

四个角落之所以打问号,是因为在这个方案中,角落写什么是无关紧要的,只要我们从第一行中间、第一列中间读取值就好了。这个做法的程序可以参见 提交 #12011

当然也可以有其他做法,如: $$ \begin{array}{ccccccc} \color{red}1 & \color{orange}? & \color{orange}? & \color{orange}? & \color{orange}\cdots & \color{orange}? & \color{blue}0 \\ \color{orange}? & a_{0, 0} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} & \color{orange}? \\ \color{orange}? & a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} & \color{orange}? \\ \color{orange}? & a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} & \color{orange}? \\ \color{orange}\vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \color{orange}\vdots \\ \color{orange}? & a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} & \color{orange}? \\ \color{blue}0 & \color{orange}? & \color{orange}? & \color{orange}? & \color{orange}\cdots & \color{orange}? & \color{blue}0 \end{array} $$ 可以根据角落的 $1$ 被翻到了什么位置知道是否被上下 / 左右翻转。

子任务 3

考虑增加的旋转 $90^\circ$ 这个操作。进行两次旋转的结果和进行一次上下翻转和一次左右翻转的结果是一样的。事实上,增加这种操作以后一共只有 $8$ 种不同的变换结果,即在原来的四种

四种不同变换结果

的基础上增加了

另外四种变换结果

观察之前的做法,例如在角落放一个 $1$ 和三个 $0$ 的做法,我们会发现后四种结果中 $1$ 最后的位置和前四种会一一对应,但是对应的两种结果中,有某种顺时针和逆时针的差异。我们可以通过增加一个顺时针逆时针看起来不同的标记,确定这个方向: $$ \begin{array}{ccccccc} \color{red}1 & \color{red}1 & \color{orange}? & \color{orange}? & \color{orange}\cdots & \color{orange}? & \color{blue}0 \\ \color{blue}0 & a_{0, 0} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} & \color{orange}? \\ \color{orange}? & a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} & \color{orange}? \\ \color{orange}? & a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} & \color{orange}? \\ \color{orange}\vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \color{orange}\vdots \\ \color{orange}? & a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} & \color{orange}? \\ \color{blue}0 & \color{orange}? & \color{orange}? & \color{orange}? & \color{orange}\cdots & \color{orange}? & \color{blue}0 \end{array} $$

解密程序可以先找到角落的 $1$,从而确定哪一个角落是原矩阵的左上角;再根据这个 $1$ 旁边的 $1$,确定哪个方向是原矩阵的列上的正方向(即 $a_{0, 0}$ 到 $a_{0, 1}$ 的方向),这样一来就可以还原原矩阵。这个做法的程序可以参见 提交 #12107

子任务 4

由于有了行列轮换,原先角落的位置不一定会保持在角落,所以子任务 3 的做法就无效了。由于不需要考虑翻转,所以我们只需要找到变换后的矩阵中原矩阵左上角的位置,就可以复原原矩阵,即变换后的矩阵一定类似于 $$ \begin{array}{ccc} \begin{array}{ccccc} \color{red}{a_{0, 0}} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} \\ a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} \\ a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} \end{array} & \Longrightarrow & \begin{array}{ccccccc} a_{p, q} & a_{p, q + 1} & \cdots & a_{p, m - 1} & a_{p, 0} & \cdots & a_{p, q - 1} \\ a_{p + 1, q} & a_{p + 1, q + 1} & \cdots & a_{p + 1, m - 1} & a_{p + 1, 0} & \cdots & a_{p + 1, q - 1} \\ \vdots & \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\ a_{n - 1, q} & a_{n - 1, q + 1} & \cdots & a_{n - 1, m - 1} & a_{n - 1, 0} & \cdots & a_{n - 1, q - 1} \\ a_{0, q} & a_{0, q + 1} & \cdots & a_{0, m - 1} & \color{red}{a_{0, 0}} & \cdots & a_{0, q - 1} \\ \vdots & \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\ a_{p - 1, q} & a_{p - 1, q + 1} & \cdots & a_{p - 1, m - 1} & a_{p - 1, 0} & \cdots & a_{p - 1, q - 1} \end{array} \end{array} $$

和之前一样,我们可以分行和列两个方向来考虑这个问题。例如要找出原矩阵的最上方在变换后的矩阵中是第几行,我们会希望让第一行有和其他行不一样的特征。例如,全部为 $1$;但是输入的矩阵也可以有某一行全部为 $1$,所以我们需要借助增加的列。这样一来,我们就可以让加密程序输出的矩阵有且只有一行全部为 $1$: $$ \begin{array}{ccccccc} \color{red}1 & \color{red}1 & \color{red}1 & \color{red}\cdots & \color{red}1 & \color{red}1 \\ a_{0, 0} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} & \color{blue}0 \\ a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} & \color{blue}0 \\ a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} & \color{blue}0 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \color{blue}\vdots \\ a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} & \color{blue}0 \\ \end{array} $$ 现在再来考虑列。我们也可以让唯一一列全部为 $1$,当然这也需要借助增加的行。我们只要保证增加的行不全为 $1$,且第一行仍全为 $1$,就可以不打乱之前对行的处理: $$ \begin{array}{ccccccc} \color{red}1 & 1 & 1 & 1 & \cdots & 1 & 1 \\ \color{red}1 & a_{0, 0} & a_{0, 1} & a_{0, 2} & \cdots & a_{0, m - 1} & 0 \\ \color{red}1 & a_{1, 0} & a_{1, 1} & a_{1, 2} & \cdots & a_{1, m - 1} & 0 \\ \color{red}1 & a_{2, 0} & a_{2, 1} & a_{2, 2} & \cdots & a_{2, m - 1} & 0 \\ \color{red}\vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ \color{red}1 & a_{n - 1, 0} & a_{n - 1, 1} & a_{n - 1, 2} & \cdots & a_{n - 1, m - 1} & 0 \\ \color{red}1 & \color{blue}0 & \color{blue}0 & \color{blue}0 & \color{blue}\cdots & \color{blue}0 & \color{blue}0 \end{array} $$ 这样,变换后的矩阵一定只有一行一列为 $1$,通过找到这样的一行一列,就可以确定原矩阵左上角的位置(事实上这个方案和子任务 2 的第一个方案很像)。这个做法的程序参见 提交 #12109

子任务 5、6

先咕着。简单思想是用异或和完成有方向的标记。(事实上,子任务 5 可能不用异或和也可以完成。)

我的一个 $(n + 1) \times (m + 1)$ 的做法可参见 提交 #9639,不过要求 $n \geq 6, m \geq 5$;我非常相信有(很恶心)的做法可以把所有大小的矩阵都用不超过 $(n + 1) \times (m + 1)$ 的矩阵编码。

@fizzydavid 的正解参见 提交 #9638

NFLSPC #1 A题 Quine Remastered 题解

2018-12-02 00:23:50 By TRCYX
quine_n

观察题目中给出的 Quine 的标程:

#include<cstdio>
char s[]="#include<cstdio>%cchar s[]=%c%s%c;int main(){printf(s,10,34,s,34);}";int main(){printf(s,10,34,s,34);}
  • 它通过 printf 一个结构和整个程序差不多的字符串来打印程序本身
  • 引用这个字符串本身的地方用 printf 的格式串 %s 代替,并在 printf 对应位置填入自身。
  • 必须通过转义才能在字符串中直接写出来的字符(如换行符 \n、引号 \"),使用格式串 %c 搭配对应字符的编码(这里都是 ASCII 码)输出。

我们可以稍微改一改 Quine 的标程,来得到这个问题的解答。

首先我们可能希望这样:

#include<cstdio>
char s[] = /* 某个字符串 */;
int main(){
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) printf(/* 用 s 打印整个程序 */);
}

缩行以后:

#include<cstdio>
char s[]=/* 某个字符串 */;int main(){int n;scanf("%d",&n);for(int i=0;i<n;++i)printf(/* 用 s 打印整个程序 */);}

这样一来,s 大概就需要长这样:

"#include<cstdio>%cchar s[]=%c%s%c;int main(){int n;scanf(%c%c%c%c,&n);for(int i=0;i<n;++i)printf(/* 用 s 打印整个程序 */);}%c"
//                ^          ^ ^ ^                         ^ ^ ^ ^                                                          ^
//               \n          " s "                         " % d "                                 保证两次输出不粘在一起 -> \n

而对应的打印方法就是:

printf(s,10,34,s,34,34,37,100,34,10);
//        ^  ^ ^  ^  ^  ^  ^   ^  ^
//       \n  " s  "  "  %  d   " \n

然后把上文中 s 中表示打印的部分同样替换成这样,我们就可以得到:

#include<cstdio>
char s[]="#include<cstdio>%cchar s[]=%c%s%c;int main(){int n;scanf(%c%c%c%c,&n);for(int i=0;i<n;++i)printf(s,10,34,s,34,34,37,100,34,10);}%c";int main(){int n;scanf("%d",&n);for(int i=0;i<n;++i)printf(s,10,34,s,34,34,37,100,34,10);}

这样就完成了第一个任务。

quine_rev

(这里偷点懒,直接搬选手程序了233)(会从不同思路中找一个最早的)

第二个任务需要把程序倒过来。如果直接套用之前的方法,我们必须将输出的结果先进行格式化,然后倒过来。

如果这样想,最简单的办法(需要一些 C 风格 IO 知识),是使用 sprintf,这个函数和 printf 类似,不过结果会输出到一个 char 数组,如 @smallfat 的 提交 #10016

char s[]="char s[]=%c%s%c,t[1000];int main(){sprintf(t,s,34,s,34);int len=strlen(t);while(len--)putchar(t[len]);return 0;}",t[1000];int main(){sprintf(t,s,34,s,34);int len=strlen(t);while(len--)putchar(t[len]);return 0;}

做法就是直接将格式化后的结果输出到 t 然后倒序输出。@XiaoGeNintendo 的 提交 #10209Java 中使用了类似的做法:

public class A {static String s="public class A {static String s=%c%s%c;public static void main(String[] args) {String ano=String.format(s,34,s,34);for(int i=ano.length()-1;i>=0;i--) {System.out.print(ano.charAt(i));}}}";public static void main(String[] args) {String ano=String.format(s,34,s,34);for(int i=ano.length()-1;i>=0;i--) {System.out.print(ano.charAt(i));}}}

@fafa 的 提交 #10025 运用了 C++11raw string literal,格式是 R"<delim>(<内容>)<delim>",其中 <delim> 是两个一样的字符串,中间无论写什么都会被当作字符串内容,包括换行等;然后直接用 string 的拼接构造输出:

#include<bits/stdc++.h>
const char*s=R"fafa(
void print()
{
    std::string res="#include<bits/stdc++.h>\nconst char*s=R\"faf";
    res+="a(";
    res+=s;
    res+=")fa";
    res+="fa\";";
    res+=s;
    std::reverse(res.begin(),res.end());
    std::cout<<res;
}
int main()
{
    print();
})fafa";
void print()
{
    std::string res="#include<bits/stdc++.h>\nconst char*s=R\"faf";
    res+="a(";
    res+=s;
    res+=")fa";
    res+="fa\";";
    res+=s;
    std::reverse(res.begin(),res.end());
    std::cout<<res;
}
int main()
{
    print();
}

下面三个提交的做法比较接近:@Graph_coders 的 提交 #10125 先将格式串倒序,再进行输出,而原来的 %c%s 也需要变成 c%s%

#include<bits/stdc++.h>
using namespace std;
char s[]="#include<bits/stdc++.h>c%using namespace std;c%char s[]=c%s%c%;int main(){reverse(s,s + strlen(s));printf(s,34,s,34,10,10);}";int main(){reverse(s,s + strlen(s));printf(s,34,s,34,10,10);}

@csy2005 的 提交 #10252 则是类似于这种做法的手动倒序:

#include<cstdio>
char ss[]="#include<cstdio>c%char ss[]=c%s%c%,s[]=c%s%c%;int main(){printf(s,34,ss,34,34,s,34,10);}",s[]="};)01,43,s,43,43,ss,43,s(ftnirp{)(niam tni;%c%s%c=][s,%c%s%c=][ss rahc%c>oidtsc<edulcni#";int main(){printf(s,34,ss,34,34,s,34,10);}

@superay 的 提交 #10362 是通过手写倒序构造正序:

#include <stdio.h>
#include <string>
#include <algorithm>
using namespace std;char s[]="};0 nruter;)01,01,01,43,)(rts_c.t,43,s(ftnirp;))(dne.t,)(nigeb.t(esrever{)(niam tni;s=t gnirts;%c%s%c=][s rahc;dts ecapseman gnisu%c>mhtirogla< edulcni#%c>gnirts< edulcni#%c>h.oidts< edulcni#";string t=s;int main(){reverse(t.begin(),t.end());printf(s,34,t.c_str(),34,10,10,10);return 0;}

@Zzzyt 的 提交 #10639 用的是 PythonPython 中也有类似 printf 的格式化,而且不会直接输出到 stdout,且取倒序更为方便:

s='s=%r\nprint((s%%s)[::-1])'
print((s%s)[::-1])
后记

这道题目本意是出一道有意思的代码谜题,因为代码谜题多少可以让大家从算法中解放一下思路,却不离开编程,也不是大模拟。(被 @skyline 吐槽是脑经急转弯)不过似乎大家都想不出什么好的,我就造了这道题。

本来我记得看到过一个有很有意思的回文程序(这个思路可以从 提交 #9304 中看到一点影子),不过那个程序的具体的要求和用处已经不太记得了,所以就在 Quine 的基础上弄了个水一点的版本,也就是倒序输出程序本身。我并没有指望大家都写回文程序,觉得大家各写各的,多提供思路还是很有意思的,不过对于直接 sprintf 还是有点百感交集,似乎有点过分直白了233。

考虑到如果直接这样出,可能很多人会想不出来,就放了 Quine 的例程。为了让大家熟悉这里面的逻辑,给第二个任务做铺垫,我就增加了 50 分的第一个任务。这个任务的解法和提供的程序差别真的不大。这样一来,从无到有的乐趣的确稍微减少了一点,不过送分程度就提高了,大家更难卡在不必要的地方。不过似乎还是有很多人卡住了,比如我看到有人在现场不会输出双引号等等。

另一方面 testlib 对换行的处理不尽如意,我本来是想,例如第一个任务,最后一行末没换行符,就要把相邻的两次输出粘在一起,这样更符合逻辑,但是 spj 写起来会很不美观,所以就放弃了。

(我倒是希望以后的 NFLSPC 中可以也有类似类型的题目,不过反正也不归我管了)

这个题解来得比较早,只要各位出题人不咕咕其他题的题解会慢慢给出的。

搜索时现在可以自动跳转到相应题目

2018-08-13 23:12:14 By TRCYX

大家可以看到导航栏里现在能直接搜索题目。原来题目列表里的搜索框就删掉了。我没怎么调样式,如果不好看欢迎评论。

现在暂定的格式是 #123 跳转到第 123 题。直接搜 123 仍然和原来一样。(不一样,原来还能搜到题号为 1234 的,现在如果不是题目名称中有 123 就搜不到了)

然后现在支持 Chrome 的 tab to search。我没有测试其他浏览器(和其他平台上的 Chrome),如果有在其它网站上能用相应功能但在这里用不了的欢迎向我反映。

如果大家对搜索题目的格式有更好的想法欢迎评论,毕竟 shift + 3 的确不太好打。

关于 Rating 图像和今天的排行榜的一些问题

2018-02-08 17:37:42 By TRCYX
为什么我没有写题的比赛也出现在 Rating 图中?

只要你报名后,点开比赛看到题目,就算参加了比赛。如果不想因为一场没有写题的比赛丢 Rating,你可以选择不报名,或者不点开看题。

最近似乎有一些同时进行的比赛,请不要参加错比赛。

为什么同时结束的比赛的 Rating 在图上可能会顺序错误?

UOJ 的比赛只会统计比赛前的 Rating,比赛后的 Rating 必须由下一场比赛前的 Rating (或者用户现在的 Rating)计算出……

同时结束的比赛在排序中只能随机,所以不知道下一场比赛前的 Rating 到底指的是哪一场比赛结束后的的 Rating,所以可能顺序会反。

不过同时结束的比赛理论上也无法说清楚应该将哪一场的 Rating 记录在用户 Rating 中。也许在以后可以改成每次比赛记录选手在这次比赛中 Rating 加减了多少。

为什么排行榜出锅了?

似乎是因为两场比赛在同时最终测试?如果找到了 UOJ 源码的问题,这个地方会填上更细致的介绍。请大家做好明天也会出锅的准备。

第二天没有出锅(?),所以暂且认为是管理员在重新测试尚未结束时就公布了成绩。我已经把这个部分的代码修改过了,重新测试未结束不可以公布成绩。

如果有其他 bug 应该怎么办?

可以联系 2915154295@qq.com 联系管理员反馈。

当然更好的办法是加入 OJ 的开发或者向管理员索要一份代码找到 bug 的位置。

怎么把博客弄成这样?

借助网站的 Bootstrap 样式,你可以在编辑器里加入这样的 HTML 代码:

            
    <div class="card my-3">
        <div class="card-header">
            <h5 class="mb-0">这是标题</h5>
        </div>
        <div class="card-body">
            <p>这是正文</p>
            <p>这是第二段正文</p>
        </div>
    </div>
            
        

关于样式更换

2018-01-29 23:26:44 By TRCYX

最近更新了 oj 的 Bootstrap UI 版本,目前看起来可以用了,虽然还有一些没有彻底改好的地方。

新样式外观会偏向扁平化,有助于内容展示。字体会默认变为无衬线字体。

评测细节还没有完全适配,这个东西的显示暂时会变丑,不过是可以调好的。

小屏幕上的访问也许会受到影响,不过 Bootstrap 本身就是适应小屏幕的,主要需要一些设置上的调整。

大家觉得是现在就放上来呢还是等到差不多完美了再放上来呢?

共 10 篇博客