COCI2021-2022 Contest1 题解
Tsawke
·
·
个人记录
COCI2021-2022 Contest1 题解
[TOC]
更好的阅读体验戳此进入
(建议您从上方链接进入我的个人网站查看此 Blog,在 Luogu 中图片会被墙掉,部分 Markdown 也会失效)
原题面链接
Luogu题面
T1 Ljeto
题意
给你 n 条信息,表示 t_i 时刻 a_i 击中 b_i ,得为 100 ,然后如果有十秒以内连续击中两次的额外加 50 分。
### Examples
**Input_1**
> 3
>
> 10 1 6
>
> 20 1 7
>
> 21 8 1
**Output_1**
> 250 100
**Input_2**
> 3
>
> 10 2 5
>
> 15 2 6
>
> 25 2 5
**Output_2**
> 400 0
**Input_3**
> 2
>
> 10 5 2
>
> 11 6 3
**Output_3**
> 0 200
### Solution
需要注意如果有十秒内连续三次击中,可以加两次 $ 50 $ 分而不是三次,即若两次间隔小于十秒但中间仍有一次击中不算加分。
另外需要注意的是在 Luogu 的翻译题面中并没有说明 $ t_i $ 升序输入,如果非升序则需要先离线排序一下,但原版题面中有这样一句
> The numbers $ t_i $ are distinct and are ordered increasingly.
保证了升序输入且不会有相同。
upd - 已向 Luogu 提交翻译错误,题面现已被修改。
将这两点想明白之后这个题就真的是一道入门题了,考虑记录每个人上次击中的时间,直接根据题意模拟即可。
```cpp
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
template<typename T = int>
inline T read(void);
int as(0), bs(0);
void shot(int n){n <= 4 ? as += 100 : bs += 100;}
void doub(int n){n <= 4 ? as += 50 : bs += 50;}
int lshot[10];
int main(){
for(int i = 0; i <= 9; ++i)lshot[i] = -114514;
int N = read();
for(int i = 1; i <= N; ++i){
int t = read(), a = read(); (void)read();
if(t - lshot[a] <= 10)doub(a);
shot(a);
lshot[a] = t;
}
printf("%d %d\n", as, bs);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template<typename T>
inline T read(void){
T ret(0);
short flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
```
## T2 Kamenčići
### 题面
一行 $ (n \le 350) $ 个石头有红蓝两种颜色,Alice & Bob 轮流从一端取一个,对于每个人当手中有 $ K $ 个红色石子时则失败,保证会有人获胜,求出两人谁获胜。
输出在保证两人均采用最优策略的情况下,谁将会取胜。若 $ \texttt{Alice} $ 胜利输出 $ \texttt{DA} $,反之输出 $ \texttt{NE} $。
### Examples
**Input_1**
> 4 1
>
> CCCP
**Output_1**
> DA
**Input_2**
> 8 2
>
> PCPPCCCC
**Output_2**
> DA
**Input_3**
> 9 1
>
> PPCPPCPPC
**Output_3**
> NE
### Solution
先说几个乱搞的做法:
首先观察发现似乎可以贪心,如果一边红色一边蓝色显然最优方案一定是取蓝色的。
对于两边都是红色或者都是蓝色,~~我有个别的贪心方案但是假掉了,~~题解里有一个贪心方案是尽量让对方更快碰到红色,也就是找到除头尾外,哪边红色石头更近,或者找哪个蓝色石头更远,按照这个思路似乎可以切掉这道题,不过我认为这个方案正确性并不显然,有可能只是运气好数据比较水。
(贪心+随机化可以过更多点,不过因为是捆测,最后似乎还会是 0pts)
回到正解,石头个数 $ n $ 满足 $ n \le 350 $,这个数据范围显然可以区间 DP。
可以考虑令 $ dp(l, r, k) $ 表示石子仅剩下 $ \left[l, \ r\right] $ 的区间,轮到的人已经拿走了 $ k $ 个红色石子(这里显然因为 Alice 先手,轮流拿顺序固定,所以不需要将谁拿石子单独设置一维),然后考虑状态转移,当从当前区间移除一个石子的时候,会变为 $ \left[l + 1, \ r\right] $ 或 $ \left[l, \ r - 1\right] $,然后取石子的人就变成了另一个,这时区间的维度已经考虑好了,就需要考虑 $ k $ 如何计算。
显然对于整个区间的红色石子是由三部分构成:区间内红色 + Alice 取走的 + Bob 取走的。
考虑用前缀和维护每个区间内的红色石子,我们又已经知道当前这个人取走的数量,那么设转移后的 $ k $ 为 $ k' $,那么就有:
$$
k' = sum(N) - ( \ sum(r) - sum(l - 1) \ ) - k
$$
考虑到对手之间获胜状态相反,所以需要取反。考虑到两人均选择最优方式挑选,所以需要或运算。
于是就会有如下状态转移方程:
$$
dp(l, r, k) \ =\ \neg dp(l + 1, r, k') \ \ \vee \ \ \neg dp(l, r - 1, k')
$$
此时考虑到边界条件就可以得出最终方程:
$$
dp(l, r, k) = \left\{
\begin{array}{ll}
false &\quad k > K \\
true &\quad k' > K \\
\neg dp(l + 1, r, k') \ \ \vee \ \ \neg dp(l, r - 1, k') &\quad otherwise
\end{array}
\right.
$$
考虑到初始化较为复杂,可以考虑 dfs + 记忆化搜索。
### Code
```cpp
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
template<typename T = int>
inline T read(void);
int N, K;
bool stone[400];
int sum[400];
int dp[400][400][400];
int DFS(int l, int r, int k){
if(~dp[l][r][k])return dp[l][r][k];
if(k >= K)return false;
int k_ = sum[N] - (sum[r] - sum[l - 1]) - k;
if(k_ >= K)return true;
return dp[l][r][k] = (!DFS(l + 1, r, k_) | !DFS(l, r - 1, k_));
}
int main(){
memset(dp, -1, sizeof(dp));
N = read(), K = read();
for(int i = 1; i <= N; ++i){
char c = getchar(); while(c != 'C' && c != 'P')c = getchar();
stone[i] = (c == 'C' ? true : false);
sum[i] = sum[i - 1] + stone[i];
}
printf("%s\n", DFS(1, N, 0) ? "DA" : "NE");
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template<typename T>
inline T read(void){
T ret(0);
short flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
```
## T3 Logičari
### 题面
对一个基环树进行染色,使每个点有且仅有一个,不包括自身的,与他相连的点被染色,求最少染色数(包括无解情况)。
$ n \le 10^5 $。
### Examples
**Input_1**
> 4
>
> 1 2
>
> 2 3
>
> 3 4
>
> 4 1
**Output_1**
> 2
**Input_2**
> 3
>
> 1 2
>
> 2 3
>
> 3 1
**Output_2**
> -1
**Input_3**
> 7
>
> 1 2
>
> 2 3
>
> 3 4
>
> 4 5
>
> 5 6
>
> 6 7
>
> 2 4
**Output_3**
> 4
### Solution
#### 思路
该说不说这题的细节是真的多,改了一下午才过了...
不过这题也挺套路,核心思路考虑把基环树拆开做树上 DP。
观察题意,首先考虑如果是普通树上的染色问题,很套路的树上 DP 即可解决,而基环树与普通树的区别,也就是多了一条边,使“树”上有且仅有一个环,那么我们的思路也就是将他转化为普通的树上问题。
于是考虑找到环上的任意一个边并将其断开,然后枚举这两个点可能的状态,并在 DP 的时候随时考虑这两个点。
#### 找环上边
一般有两种方法,一种是维护并查集,当新的边连接的两个节点,是同一颗子树上的时候,要找的就是这个边。
另一种方式更简便一些,DFS 遍历整个树,当访问到了非父亲节点,但曾经访问过的节点时便说明这个边是环上的边。
```cpp
void FindLoop(int p, int fa){
for(auto i = head[p]; i; i = i->nxt){
if(i->to != fa && vis[i->to]){loop = make_pair(p, i->to); return;}
if(i->to != fa){vis[i->to] = true; FindLoop(i->to, p);}
}
}
```
#### 删边
如果用的并查集维护,直接记录下并不将这个边存到树里即可。
如果用的 DFS,那么可以考虑直接遍历找到点删除,或每次调用的时候都判断是否为删掉的这个边。
```cpp
void RemoveLoop(void){
for(auto i = head[loop.first], lasti = (Edge*)npt; i; lasti = i, i = i->nxt){
if(i->to == loop.second){
lasti
? lasti->nxt = i->nxt
: head[loop.first] = i->nxt;
break;
}
}
for(auto i = head[loop.second], lasti = (Edge*)npt; i; lasti = i, i = i->nxt){
if(i->to == loop.first){
lasti
? lasti->nxt = i->nxt
: head[loop.second] = i->nxt;
break;
}
}
tie(root1, root2) = loop;
}
```
#### 状态设计
考虑在普通的树形 DP 中考虑被分割的两个节点,这里定义为 $ root1 $ 和 $ root2 $。
考虑令被染色为 $ true $,未被染色为 $ false $。
设 $ dp(i, j, k, l, m) $ 表示当前计算到了 $ i $ 节点,其状态和其父亲状态分别为 $ j, k $,两个根的状态分别为 $ l,m $。
因为每个点有且只有一个与之相连的节点会被染色,所以我们可以考虑先假设当前节点所有子节点都不染色,并计算求和,然后分别枚举其每一个子节点,计算如果将该子节点涂色最终需要涂多少点,并取最小值。
但是这题的最大难点我认为就是上面这些过程中合法性的判断,也就是细节的处理。
同时因为状态十分复杂,考虑用 DFS + 记忆化实现。
#### 细节处理
首先我们需要考虑,哪些状态是不可能出现的:
1. 遍历到某个根节点,但当前状态与根节点已经定下来的状态不同。
2. 遍历到某个根节点,父亲节点已被染色,且两个根节点都被染色,导致其中某个根节点,考虑上被临时删除的边之后有两个相连的点被染色。
```cpp
if(
(currentPosition == root1 && currentStatus != root1Status) ||
(currentPosition == root2 && currentStatus != root2Status) ||
(currentPosition == root1 && fatherStatus && root2Status) ||
(currentPosition == root2 && fatherStatus && root1Status)
)return DEFAULT_DP = INF;
```
然后我们需要考虑,什么时候当前的节点的子节点都不能被染色:
1. 父节点已经被染色,即当前节点已经有了一个节点与之相连且被染色。
2. 当前节点到了某一个根节点,而另一个根节点已被染色,与 1 同理。
```cpp
if(
fatherStatus ||
(currentPosition == root1 && root2Status) ||
(currentPosition == root2 && root1Status)
) ret = min(ret, sonCost);
```
还有个很重要的点就是我们假设都不染色进行求和操作的时候会爆 $ int $ 所以需要在求和时需要开 $ long \ \ long $。
#### 主函数
回到我们之前说的,要枚举两个根节点的状态,我们可以考虑令其从其中某个根节点开始遍历,显然会简便很多,显然一共可能有如下四种情况。
```cpp
int ans = min(
{
Tintage(root1, 0, 0, 0, 0, -1),
Tintage(root1, 0, 0, 0, 1, -1),
Tintage(root1, 1, 0, 1, 0, -1),
Tintage(root1, 1, 0, 1, 1, -1),
INF
}
);
```
### Code
```cpp
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define INF 11451400
#define DEFAULT_DP dp[currentPosition][currentStatus][fatherStatus][root1Status][root2Status]
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
template<typename T = int>
inline T read(void);
int N;
bool vis[110000];
pair < int, int >/*from, to*/ loop;
int root1, root2;
struct Edge{
Edge* nxt;
int to;
void* operator new(size_t);
Edge(Edge* nxt, int to):nxt(nxt), to(to){;}
Edge(void) = default;
}eData[210000];
void* Edge::operator new(size_t){static Edge* P = eData; return ++P;}
Edge* head[110000];
int dp[110000][2][2][2][2]; /*CurrentPosition, CurrentStatus, FatherStatus, Root1Status, Root2Status*/
void FindLoop(int = 1, int = -1);
void RemoveLoop(void);
int Tintage(int, bool, bool, bool, bool, int);
int main(){
memset(dp, -1, sizeof(dp));
N = read();
for(int i = 1; i <= N; ++i){
int from = read(), to = read();
head[from] = new Edge(head[from], to);
head[to] = new Edge(head[to], from);
}
FindLoop();
RemoveLoop();
int ans = min(
{
Tintage(root1, 0, 0, 0, 0, -1),
Tintage(root1, 0, 0, 0, 1, -1),
Tintage(root1, 1, 0, 1, 0, -1),
Tintage(root1, 1, 0, 1, 1, -1),
INF
}
);
printf("%d\n", ans == INF ? -1 : ans);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
int Tintage(int currentPosition, bool currentStatus, bool fatherStatus, bool root1Status, bool root2Status, int fatherPosition){
if(~DEFAULT_DP)return DEFAULT_DP;
if(
(currentPosition == root1 && currentStatus != root1Status) ||
(currentPosition == root2 && currentStatus != root2Status) ||
(currentPosition == root1 && fatherStatus && root2Status) ||
(currentPosition == root2 && fatherStatus && root1Status)
)return DEFAULT_DP = INF;
ll sonCost(currentStatus);
for(auto i = head[currentPosition]; i; i = i->nxt)
if(i->to != fatherPosition)
sonCost += Tintage(i->to, false, currentStatus, root1Status, root2Status, currentPosition);
ll ret(INF);
if(
fatherStatus ||
(currentPosition == root1 && root2Status) ||
(currentPosition == root2 && root1Status)
) ret = min(ret, sonCost);
else
for(auto i = head[currentPosition]; i; i = i->nxt)
if(i->to != fatherPosition)
ret = min({
ret,
(ll)INF,
sonCost - Tintage(i->to, false, currentStatus, root1Status, root2Status, currentPosition)
+ Tintage(i->to, true, currentStatus, root1Status, root2Status, currentPosition)
});
return DEFAULT_DP = ret;
}
void RemoveLoop(void){
for(auto i = head[loop.first], lasti = (Edge*)npt; i; lasti = i, i = i->nxt){
if(i->to == loop.second){
lasti
? lasti->nxt = i->nxt
: head[loop.first] = i->nxt;
break;
}
}
for(auto i = head[loop.second], lasti = (Edge*)npt; i; lasti = i, i = i->nxt){
if(i->to == loop.first){
lasti
? lasti->nxt = i->nxt
: head[loop.second] = i->nxt;
break;
}
}
tie(root1, root2) = loop;
}
void FindLoop(int p, int fa){
for(auto i = head[p]; i; i = i->nxt){
if(i->to != fa && vis[i->to]){loop = make_pair(p, i->to); return;}
if(i->to != fa){vis[i->to] = true; FindLoop(i->to, p);}
}
}
template<typename T>
inline T read(void){
T ret(0);
short flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
```
## T4 Set
>## 题目背景
>
>在知名游戏 $ \texttt{SET} $ 中,存在着一些数字、形状、颜色等不同的卡片,玩家的目标是确定一个存在的 $ \texttt{triplet of cards} $(即卡片的三元组,也就是三张卡片构成的组合),使其符合特定的要求。 $ \texttt{Marin} $ 和 $ \texttt{Josip} $ 很快就对这个游戏感到无趣,并对其进行了加强。
>
>## 题目描述
>
>在本题中,定义每张卡片代表着一个仅由 $ 1, 2, 3 $ 构成的长度为 $ k $ 的序列,共有 $ n $ 张卡片,卡片之间是无序的。
>
>定义一个 $ \texttt{SET} $ 表示,当且仅当一个无序的 $ \texttt{triplet of cards} $ 其中的三个序列的每一位均相同或各不相同,用原文中的话就是 $ \texttt{same} $ 或 $ \texttt{pairwise different} $,更严谨地表示,我们令这三个序列为 $ S_i, S_j, S_k $,则一定满足如下条件:
>
>* $ i \lt j \lt k
-
\forall x \in \left[1, k\right] $,满足 $ S_i(x) = S_j(x) = S_k(x) $ 或 $ S_i(x) \neq S_j(x) \neq S_k(x)
例如 (1123, 1322, 1221) 便满足 1, 3 位均相同, 2,4 位各不相同。
给你这些序列,求可以组成多少种本质不同的 \texttt{SET} 。
输入格式
第一行为两个整数正整数 n, k 。
接下来 n 行中每一行包含一个仅由 1, 2, 3 构成的长度为 k 的序列,代表着一张卡片。
保证每张卡片上的序列不同。
输出格式
仅一行一个整数,表示可以组成的本质不同的 \texttt{SET} 的数量。
说明 / 提示
样例 3 解释
可以组成的两个 \texttt{SET} 分别为 (S_1, S_2, S_3) 和 (S_1, S_4, S_5) 。
数据范围
对于全部数据,满足 1 \le k \le 12, 1 \le n \le 3^k , S_i 互不相同且满足 1 \le S_i \le 3 。
| Subtask |
特殊限制 |
分数 |
| 1 |
k \le 5 |
10 |
| 2 |
k \le 7 |
30 |
| 3 |
无特殊限制 |
70 |
题面(重新翻译了一个新的题面,已经提交到 Luogu)
Examples
Input_1
3 4
1123
1322
1221
Output_1
1
Input_2
2 2
11
22
Output_2
0
Input_3
5 3
111
222
333
123
132
Output_3
2
Solution
需要用到很多 FWT 思想,阅读之前请先完全理解 FWT, k 进制 FWT,及用转移矩阵进行变换的 FWT。
戳此链接去看刚写完的 FWT
观察题意,发现对于每个 SET 的同一位置的三个数,要么是完全相同的三个数,要么分别是 1, 2, 3 (unordered) 。
思考这个规律有什么性质
性质1
显然设这三个数为 m_{i, x}, m_{j, x}, m_{k, x} 一定有:
m_{i, x} + m_{j, x} + m_{k, x} \equiv 0 \quad (\bmod{3})
证明:这么显然还需要证明吗
性质2
当这个 SET 中的两个序列被确定之后,第三个序列就有且仅有一种情况,或者说 uniquely \ \ determined 。
证明:一共就三个可能的数,这个也很显然吧
然后我们发现这两个性质无法继续向下推,于是我们考虑假设存在一个自洽的代数系统 S ,其中只有 1, 2, 3 三个数字,定义 S 中一种二元运算 \circ ,为了令其符合结合律,我们考虑使其符合以下规律及交换律:
\begin{aligned}
&1 \circ 1 = 1\\
&2 \circ 2 = 3\\
&3 \circ 3 = 2\\
&1 \circ 2 = 2\\
&1 \circ 3 = 3\\
&2 \circ 3 = 1
\end{aligned}
同时我们考虑定义一元运算符 \bmod{3} ,其运算规律与代数系统 (\mathbb{N}, \bmod{3}) ,即自然数和 \bmod{3} 构成的代数系统相同,详细地表示即为:
\begin{aligned}
&1 \bmod{3} = 1\\
&2 \bmod{3} = 2\\
&3 \bmod{3} = 0
\end{aligned}
此时我们会发现更多的性质:
性质3
一个数与其自身进行两次 \circ 运算后数值不改变。即:
a \circ a \circ a = a
证明:根据 \circ 的定义可知十分显然。
性质4
运算 \circ 的交换律与结合律。
证明:显然成立。
性质5
对于性质1,由新的定义可以转化为如下式子:
(\ m_{i, x} \circ m_{j, x} \circ m_{k, x}\ ) \bmod{3} = 1
也就是:
m_{i, x} \circ m_{j, x} \circ m_{k, x} = 1
此时根据这些性质,如果我们令 F(x) 表示题意中有多少种不同的 triplet 符合对于 \forall x \in \left[ 1, m \right] 都有:
m_{i, x} \circ m_{j, x} \circ m_{k, x} = x
则显然 F(0) 表示所有不同的 SET 的数量。
(注意这里并不是本质不同,所以也可以理解为 triplet 中三个序列的排列)
这里我们定义序列 g_n ,有:
g_i = \left\{
\begin{array}{ll}
0 &\quad unexist \\
1 &\quad exist
\end{array}
\right.
需要注意下标 i 表示的不是一个数字,而是一个序列,由数据范围可以考虑按位存取,最简单的想法是类似离散化,把它按三进制,压成一个数,也可以考虑用两位二进制存这个数。
存在与否指的是是否在题目给出的 n 个序列中。
容易看出会有如下式子:
F(x) = \sum_{i \circ j \circ k = x}g_i \times g_j \times g_k
当然这个式子也可以记作:
F = g \times g \times g
而我们需要的便是这个式子的常数项 F(1) ,注意这里因为我们的代数系统中不存在 0 ,但如果我们用三进制离散化压位来存的话,最终需要的就是 F(0) ,即:
F(0) = \sum_{i \circ j \circ k = 0}g_i \times g_j \times g_k
仔细观察一般的式子,这像什么?显然是多项式的各种快速变换!或者进一步说,很像 FWT。
FWT(快速沃尔什变换)一般用于处理形如如下式子的卷积:
C(x) = \sum_{i \circ j = x}A(i) \times B(j)
此处的 \circ 一般为 \&, |, ^, 也就是 and, or, xor 。
发现我们当前的式子很像 FWT,所以也可以以 FWT 的思想去考虑本题。
FWT 的思路
与大多数多项式快速变换的思路一样,我们的目的都是找到一种变换,对于 FWT 可以考虑记作:
FWT(A)
我们需要让这个变换满足以下性质:
FWT(A) \ast FWT(B) = FWT(C)
且:
A \circ B = C
对于不同的运算都有着与之对应的不同的变换方式,我们的目的就是要找到一种优秀的变换并快速地进行变换。
这里额外说一下对于多项式约定俗成的几种运算表示什么,相信你们一定都知道(主要因为我最开始做这道题的时候有的符号理解错了)
这里我们假设 A 最高次为 N 次, B 最高次为 M 次。
令 \max(N, M) = P 。
\begin{aligned}
&(A, B) = (\ A(0), A(1), \cdots, A(N), B(0), B(1), \cdots, B(M) \ ) \ \texttt{即拼接两个多项式} \\
&A + B = (\ A(0) + B(0), A(1) + B(1), \cdots, A(P) + B(P) \ ) \ \texttt{即按位相加} \\
&A - B = (\ A(0) - B(0), A(1) - B(1), \cdots, A(P) - B(P) \ ) \ \texttt{即按位相减} \\
&A \ast B = (\ A(0) \ast B(0), A(1) \ast B(1), \cdots, A(P) \ast B(P) \ ) \ \texttt{即按位相乘,注意不是卷积} \\
&A \times B = (\ \sum_{i + j = 0}A(i) \times B(j), \sum_{i + j = 1}A(i) \times B(j), \cdots, \sum_{i + j = N + M}A(i) \times B(j) \ ) \ \texttt{多项式卷积} \\
&A \circ B = (\ \sum_{i \circ j = 0}A(i) \times B(j), \sum_{i \circ j = 1}A(i) \times B(j), \cdots \ ) \ \texttt{可以理解为广义上的卷积}
\end{aligned}
本题如何做?
通过上面的信息显然我们便可确定本题的核心:找出在代数系统 S 中运算 \circ 的卷积运算时的广义上的 FWT 变换。
从哪入手呢?观察对于常用的三个运算的 FWT 变换式子,我们就会发现这些式子的存在本身就充满着人类智慧,似乎不像是可以很简单推导出来的,但是如果我们从矩阵的角度去考虑,并基于已有的运算的转移矩阵,就可以较为方便的得出结论。
如何推式子?
再次观察我们定义的这个二元运算符 \circ :
\begin{aligned}
&1 \circ 1 = 1\\
&2 \circ 2 = 3\\
&3 \circ 3 = 2\\
&1 \circ 2 = 2\\
&1 \circ 3 = 3\\
&2 \circ 3 = 1
\end{aligned}
题外话:写到这里突然发现我好像推不出来这个式子,于是决定先去把 FWT 的坑填了...
我们要求的可以理解为是以下的式子:
C(x) = \sum_{i \circ j = x}A(i) \times B(j)
用和 FWT 一样的思想,因为我们只有三个数,所以可以考虑构造一个 3 \times 3 的转移矩阵,但是注意我们定义的运算 \circ 下标从 1 开始,所以与标准的矩阵可能有些差距。
我们需要保证对于转移矩阵 T 的 (i, j) 元,记作 \omega(i, j) ,满足:
\omega(x, i) \times \omega(x, j) = \omega(x, k) \quad (i \circ j = k)
观察运算的性质,和三进制下的异或运算性质较为相似,可以考虑尝试范德蒙德矩阵:
\begin{bmatrix}
\neg\exists & \neg\exists & \neg\exists & \neg\exists & \\
\neg\exists & 1 & 1 & 1 \\
\neg\exists & 1 & \omega_3^1 & \omega_3^2 \\
\neg\exists & 1 & \omega_3^2 & \omega_3^4 \\
\end{bmatrix}
可以化简为:
\begin{bmatrix}
\neg\exists & \neg\exists & \neg\exists & \neg\exists & \\
\neg\exists & 1 & 1 & 1 \\
\neg\exists & 1 & \omega_3 & \omega_3^2 \\
\neg\exists & 1 & \omega_3^2 & \omega_3 \\
\end{bmatrix}
逆矩阵同理容易得出为:
\dfrac{1}{3} \begin{bmatrix}
\neg\exists & \neg\exists & \neg\exists & \neg\exists & \\
\neg\exists & 1 & 1 & 1 \\
\neg\exists & 1 & \omega_3^{-1} & \omega_3^{-2} \\
\neg\exists & 1 & \omega_3^{-2} & \omega_3^{-1} \\
\end{bmatrix}
可以化简为:
\dfrac{1}{3} \begin{bmatrix}
\neg\exists & \neg\exists & \neg\exists & \neg\exists & \\
\neg\exists & 1 & 1 & 1 \\
\neg\exists & 1 & \omega_3^2 & \omega_3 \\
\neg\exists & 1 & \omega_3 & \omega_3^2 \\
\end{bmatrix}
显然我们可以算出:
\begin{aligned}
\omega_3
&= \cos(\dfrac{2 \pi}{3}) + \sin(\dfrac{2 \pi}{3})i \\
&= -\dfrac{1}{2} + \dfrac{\sqrt{3}}{2}i
\end{aligned}
且:
\begin{aligned}
\omega_3^2
&= (-\dfrac{1}{2} + \dfrac{\sqrt{3}}{2}i)^2 \\
&= -\dfrac{1}{2} - \dfrac{\sqrt{3}}{2}i
\end{aligned}
到此我们便可以求出来最终的结果了。
这里还有两个小细节需要注意:
两个小细节
首先我们在做完 IFWT 之后需要乘一个 \dfrac{1}{3} ,我们当然可以每次都做一个除法,但是观察发现,项数需要满足 3^n 形式,而层数也就是 \log_3^{3^n} = n ,所以也可以在最后答案除一个 3^n (和 FFT 挺像的),不过属于常数优化,差别不大。
然后还需要注意当我们算出来常数项之后,并不能直接输出,观察一下性质3:
a \circ a \circ a = a
在运算的时候我们显然会把 i = j = k 的情况算在内了,而显然这是不合法的,所以需要减掉 N 。
并且,我们在运算的时候求的是不同,而非本质不同,也就是算的是排列,而我们要求的是组合,所以最后除一个 3! ,也就是 6 。
综上所述,我们将常数项算出来后最终答案就是 \dfrac{F(0) - N}{6} 。
至此,这道卡了我两天多的题,终于结束了。
(记得开 long long)
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define comp complex < long double >
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
template<typename T = int>
inline T read(void);
inline int read3(void);
int N, M;
// 3^12 = 531441
comp poly[1100000];
comp omega(-0.5, 0.5 * sqrt(3));
comp omega2(conj(omega));
enum pattern{IFWT = 0, _FWT};
void FWT(comp*, int, pattern);
int main(){
N = read(), M = read();
for(int i = 1; i <= N; ++i)poly[read3()].real(1.0);
int lim(1), cnt(0);
while(cnt++ < M)lim *= 3;
FWT(poly, lim, _FWT);
for(int i = 0; i < lim; ++i)poly[i] = poly[i] * poly[i] * poly[i];
FWT(poly, lim, IFWT);
ll ans = (poly[0].real() / (long double)lim) + 0.5;
printf("%lld\n", (ans - N) / 6);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
void FWT(comp* poly, int lim, pattern pat){
for(int len = 1; len < lim; len *= 3)
for(int px = 0; px < lim; px += 3 * len)
for(int p = 0; p < len; ++p){
int pos1(px + p + len * 0),
pos2(px + p + len * 1),
pos3(px + p + len * 2);
comp pol1 = poly[pos1];
comp pol2 = poly[pos2];
comp pol3 = poly[pos3];
if(pat == _FWT){
poly[pos1] = pol1 + pol2 + pol3;
poly[pos2] = pol1 + pol2 * omega + pol3 * omega2;
poly[pos3] = pol1 + pol2 * omega2 + pol3 * omega;
}else{
poly[pos1] = pol1 + pol2 + pol3;
poly[pos2] = pol1 + pol2 * omega2 + pol3 * omega;
poly[pos3] = pol1 + pol2 * omega + pol3 * omega2;
}
}
}
inline int read3(void){
int ret(0);
char c = getchar();
while(!isdigit(c))c = getchar();
while(isdigit(c)){
ret *= 3;
ret += int(c - '0' - 1);
c = getchar();
}
return ret;
}
template<typename T>
inline T read(void){
T ret(0);
short flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
T5 Volontiranje
这题比 T4 简单多了。
题面
给定一个长度为 n 的排列,求最多的不交叉最长上升子序列的(即每个数只能用一次)。(是的就这么简洁)
输出个数,长度,并输出每一个最长上升子序列。 n \le 10^6 。
Examples
Input_1
3
1 2 3
Output_1
1 3
1 2 3
Input_2
4
4 3 2 1
Output_2
4 1
1
2
3
4
Input_3
7
2 1 6 5 7 3 4
Output_3
2 3
1 3 5
2 6 7
Solution
考虑求 \texttt{LIS} 的长度直接 O(n \log n) 求即可,可以用 lower_bound 求。
而对于有哪些 \texttt{LIS} ,我们则需要找到其中的一些性质,考虑将每个数的下标作为 x ,数值作为 y ,把每个数丢到二维坐标系里面观察性质,比如对于 Input_3 ,最后会形成下图:
观察这个奇怪的图形我们可以考虑从 x = 1 开始划分层级,把 x 递增而 y 递减的点与初始点划分到同一层级。
也就是如下:
注意这里的层级划分是左下开右上闭的。
于是我们便可以发现,对于每一个 LIS 都应该是从每一个层级中选择一个点并且符合,下一层级的点符合在当前点的右上。
这里我们考虑如何分层,考虑当我们计算 LIS 时,一般用的状态是,以当前点为结尾的 LIS 长度,我们观察发现,第一层级里, A, B 长度一定为 1 ,第二层级里, C, D, F 长度一定为 2 , E, G 长度一定为 3 。
于是我们便可以发现按照 LIS_i = k ,对于同一个 k 的所有下标 i 作为同一层级。
让后我们考虑如何选择每一层级的点,这里我们有一个结论,对于每一层级优先选择纵横坐标,也就是下标更低的未选择过的点一定是更优的,这个正确性可以去举例理解一下,如果对于上图的情况,连结 AD 与 BC 和链结 AC 与 BD 都可以符合要求,是两组不同的合法解,但是考虑如下情况:
(这里为了方便表述省略了一些点)
显然层级划分的线大概是这样,这个时候如果我们对于 A ,使其连结横坐标更小的 C ,会使 BD 也是一组合法解,而如果连结 AD ,则 BC 不合法。故显然优先连结未选择里面横坐标更小的是最优方案。
至此我们的推导已经结束,可以进行实现了。
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
/******************************
abbr
******************************/
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
template<typename T = int>
inline T read(void);
vector < int > LISv;
pair < int, int > LIS[1100000];
int N;
vector < int > current;
vector < int > tier[1100000];
int arr[1100000];
vector < vector < int > > anss;
int main(){
N = read();
for(int i = 1; i <= N; ++i){
arr[i] = read();
if(LISv.empty() || LISv.back() < arr[i])LISv.push_back(arr[i]), tier[(int)LISv.size()].push_back(i);
else{
auto pos = lower_bound(LISv.begin(), LISv.end(), arr[i]);
int len = distance(LISv.begin(), pos) + 1;
*pos = arr[i];
tier[len].push_back(i);
}
}
int maxLen = (int)LISv.size();
for(int i = 1; i <= maxLen; ++i)
reverse(tier[i].begin(), tier[i].end());
while(true){
if(current.empty()){
if(tier[1].empty())break;
current.push_back(tier[1].back());
tier[1].pop_back();
}else if((int)current.size() == maxLen){
anss.push_back(current);
current.clear();
}else{
int pos = current.size() + 1;
int last = current.back();
while(!tier[pos].empty() && tier[pos].back() < last)tier[pos].pop_back();
if(tier[pos].empty() || arr[tier[pos].back()] < arr[last])current.pop_back();
else current.push_back(tier[pos].back()), tier[pos].pop_back();
}
}
printf("%d %d\n", (int)anss.size(), maxLen);
for(auto i : anss){
for(auto j : i)printf("%d ", j);
printf("\n");
}
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template<typename T>
inline T read(void){
T ret(0);
short flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
UPD
update-2022_08_30 T1-T3
update-2022_09_01 完成一部分的 T4
update-2022_09_02 T4 肝完
update-2022_09_04 初稿
update-2022_09_04 发现 T4 之前算法假掉了,修改了一下