1. 题面

有一个大小为 \(n\) (\(n\le10^6\))的方阵 \(A\),给定 \(d_1,d_2,d_3,\dots,d_n\),\((p_2,b_2,c_2),(p_3,b_3,c_3),\dots,(p_n,b_n,c_n)\) 以及 \(x\)。其中保证 \(p_i\lt i\)。\(A\) 满足:

\[A_{ij}=\begin{cases}d_i&i=j\\b_i&i=p_j\\c_i&j=p_i\\x&otherwise\end{cases}
\]

求 \(A\) 的行列式对 \((10^9+7)\) 取模的结果。


2. 解析

2.1. 拆分矩阵

和另外一道题很像……方阵的大多数位置都是 \(x\),可以想到分离出一个全为 \(x\) 的方阵 \(B\) ——

\[A=A'+B
\]

于是可以得到一个稀疏方阵 \(A'\)。

考虑行列式 \(|M|\) 的定义:枚举一个排列 \((q_1,q_2,\dots,q_n)\),贡献为 \((-1)^{\pi(q)}\prod M_{i,q_i}\)。这道题就是:

\[\sum_{q}(-1)^{\pi(q)}\prod_{i}(A'_{i,q_i}+B_{i,q_i})
\]

2.2. 树的情况

不妨先假设 \(x=0\),则只考虑 \(A'\) 对行列式的贡献。根据题意,\(A'\) 是个有值的位置关于主对角线对称的方阵,这像什么?一棵树的邻接表?具体的说,是一棵每条边都有正反向且每个点有自环的“树”。

于是我们尝试把行列式的求解搬到树上来。行列式计算时可以看作每行每列恰好选一个元素,那么选择的 \(A'_{i,q_i}\) 相当于选择了树上的一条边,由于每行每列恰选一个元素,所以每个点的出入度都为 \(\mathbf1\) —— 每个点都属于一个简单有向环

这样的“树”上,有向环只可能是父亲与儿子的二元环以及自环。我们可以做一个树形 DP 来把每个点划分到一个环中并计算贡献。但还有一个问题,行列式还有 \((-1)^{\pi(q)}\) 的系数,需要进行一些转化。

\(\pi(q)\) 的奇偶性和「交换任意两个数,将 \(q\) 变为有序的操作次数」的奇偶性相同。考虑在树上合法的排列 \(q\) 的性质,我们刚才提到把树划分成若干个,环在排列(这里用一下置换里的一些定义)里就是一个循环。要把一个排列操作为有序只需要让它的每个循环都有序,注意到一个长为 \(L\) 的循环我们可以通过 \(L-1\) 次操作把它变为有序;所以 \(\pi(q)\) 的奇偶性就和「偶环个数」的奇偶性相同。

更进一步的,\(\pi(q)\) 的奇偶性和「\(n\) 减去环个数」的奇偶性相同,这样每新增一个环就乘上 \(-1\),更加方便树形 DP。

2.3. 非树边

现在考虑另一个方阵 \(B\),同样把它看成邻接矩阵,那么它是一个边权为 \(x\) 的完全图。

观察行列式的定义式:

\[\sum_{q}(-1)^{\pi(q)}\prod_{i}(A'_{i,q_i}+B_{i,q_i})
\]

每个 \((i,q_i)\) 要么选 \(A'\) 要么选 \(B\)。也就是说选择树边时也可以选择权为 \(x\),也可以选择全为 \(x\) 的非树边。但是如果考虑非树边,环的情况就非常复杂,我们是否需要考虑这些复杂的情况呢?

接下来就是一些数学的分析,想到这一步可能需要一些经验吧……

如果在一个选择环边的方案中选择了两条权为 \(x\) 的边(多于两条则考虑最后两条),也就是在矩阵上选择了 \(B_{a,q_a},B_{b,q_b}\),我们可以“交换”一下,选择 \(B_{a,q_b},B_{b,q_a}\)。环的变化如下图,会减少一个环,意味着贡献系数相反:

由于交换操作是可逆的,这两张图一一对应,而贡献系数相反,会被抵消。这也是为什么一开始要分离出一个全为 \(x\) 的方阵 \(B\) 的原因 —— 要保证交换过后的图存在,既然 \(B\) 形成完全图,那么这张图必然存在。

于是我们只需要考虑至多选择了一条 \(x\) 边的情况,也即至多选择一条非树边,这也可以用树形 DP 计算。情况比较复杂,参考代码写得比较丑陋,建议自己想……


3. 小结

矩阵大多数位置值一样时可以拆成一个稀疏矩阵 \(A'\) 和另一个值全部相同的矩阵 \(B\)。

求解行列式又多了一个新方法了 awa:

  • 当稀疏矩阵 \(A'\) “特别稀疏”时可以状压 DP;
  • 也可以把矩阵看成邻接矩阵,此题保证了 \(p_i\lt i\),所以邻接矩阵是一棵树。

应该更注意矩阵的对称性,此题 \(A'\) 有值的位置关于主对角线对称,与邻接矩阵相似。


4. 参考代码

点击展开/折叠 特别丑的参考代码
/* Lucky_Glass */
#include <cstdio>
#include <cstring>
#include <cassert>
#include <algorithm> const int MOD = 1e9 + 7; inline int add(int a, const int &b) { return (a += b) >= MOD ? a - MOD : a; }
inline int sub(int a, const int &b) { return (a -= b) < 0 ? a + MOD : a; }
inline int mul(const int &a, const int &b) { return int(1ll * a * b % MOD); }
int pPow(int a, int b) {
int r = 1;
while (b) {
if (b & 1) r = mul(r, a);
a = mul(a, a), b >>= 1;
}
return r;
}
#define OPERON(a, b, fun) a = fun(a, b) const int N = 1e6 + 10; struct Graph {
int head[N], to[N << 1], nxt[N << 1], val[N << 1];
int edg_cnt;
inline void addEdge(const int &u, const int &v, const int &l) {
int p = ++edg_cnt;
to[p] = v, val[p] = l;
nxt[p] = head[u], head[u] = p;
}
inline int operator [] (const int &u) const { return head[u]; }
Graph() { edg_cnt = 1; }
} gr; int n, valx;
int vald[N];
int f[N][4][2]; void dfs(const int &u, const int &fa) {
int u_emp[2] = {1, 0}, u_use[2] = {}, u_up[2] = {}, u_dn[2] = {};
for (int it = gr[u]; it; it = gr.nxt[it]) if (gr.to[it] != fa) {
int v = gr.to[it]; dfs(v, u);
int tmp_emp[2] = {}, tmp_use[2] = {}, tmp_up[2] = {}, tmp_dn[2] = {};
int tov = gr.val[it], tou = gr.val[it ^ 1]; /* empty + empty */
OPERON(tmp_emp[0], mul(u_emp[0], f[v][0][0]), add);
OPERON(tmp_emp[1], mul(u_emp[1], f[v][0][0]), add);
OPERON(tmp_emp[1], mul(u_emp[0], f[v][0][1]), add); /* two-point loop */
OPERON(tmp_use[0], mul(mul(u_emp[0], f[v][1][0]), mul(tov, tou)), sub);
OPERON(tmp_use[1], mul(mul(u_emp[1], f[v][1][0]), mul(tov, tou)), sub);
OPERON(tmp_use[1], mul(mul(u_emp[0], f[v][1][1]), mul(tov, tou)), sub);
/* used + empty */
OPERON(tmp_use[0], mul(u_use[0], f[v][0][0]), add);
OPERON(tmp_use[1], mul(u_use[1], f[v][0][0]), add);
OPERON(tmp_use[1], mul(u_use[0], f[v][0][1]), add);
/* up */
OPERON(tmp_use[1], mul(mul(u_emp[0], f[v][2][0]), mul(valx, tou)), sub);
/* down */
OPERON(tmp_use[1], mul(mul(u_emp[0], f[v][3][0]), mul(valx, tov)), sub);
/* lca */
OPERON(tmp_use[1], mul(mul(u_up[0], f[v][3][0]), mul(tov, valx)), sub);
OPERON(tmp_use[1], mul(mul(u_dn[0], f[v][2][0]), mul(tou, valx)), sub); /* go up */
OPERON(tmp_up[0], mul(mul(u_emp[0], f[v][2][0]), tou), add);
OPERON(tmp_up[1], mul(mul(u_emp[1], f[v][2][0]), tou), add);
OPERON(tmp_up[1], mul(mul(u_emp[0], f[v][2][1]), tou), add);
/* up + empty */
OPERON(tmp_up[0], mul(u_up[0], f[v][0][0]), add);
OPERON(tmp_up[1], mul(u_up[1], f[v][0][0]), add);
OPERON(tmp_up[1], mul(u_up[0], f[v][0][1]), add); /* go down */
OPERON(tmp_dn[0], mul(mul(u_emp[0], f[v][3][0]), tov), add);
OPERON(tmp_dn[1], mul(mul(u_emp[1], f[v][3][0]), tov), add);
OPERON(tmp_dn[1], mul(mul(u_emp[0], f[v][3][1]), tov), add);
/* down + empty */
OPERON(tmp_dn[0], mul(u_dn[0], f[v][0][0]), add);
OPERON(tmp_dn[1], mul(u_dn[1], f[v][0][0]), add);
OPERON(tmp_dn[1], mul(u_dn[0], f[v][0][1]), add); u_emp[0] = tmp_emp[0], u_emp[1] = tmp_emp[1];
u_use[0] = tmp_use[0], u_use[1] = tmp_use[1];
u_up[0] = tmp_up[0], u_up[1] = tmp_up[1];
u_dn[0] = tmp_dn[0], u_dn[1] = tmp_dn[1];
} /* self loop */
OPERON(f[u][0][1], mul(u_emp[0], valx), sub);
OPERON(f[u][0][1], mul(u_emp[1], vald[u]), sub);
OPERON(f[u][0][0], mul(u_emp[0], vald[u]), sub);
/* others */
OPERON(f[u][0][0], u_use[0], add);
OPERON(f[u][0][1], u_use[1], add); /* two-point loop with fa */
OPERON(f[u][1][0], u_emp[0], add);
OPERON(f[u][1][1], u_emp[1], add); /* go up */
OPERON(f[u][2][0], u_up[0], add);
OPERON(f[u][2][1], u_up[1], add);
/* start from u */
OPERON(f[u][2][0], u_emp[0], add);
OPERON(f[u][2][1], u_emp[1], add); /* go down */
OPERON(f[u][3][0], u_dn[0], add);
OPERON(f[u][3][1], u_dn[1], add);
/* end at u */
OPERON(f[u][3][0], u_emp[0], add);
OPERON(f[u][3][1], u_emp[1], add);
}
template<typename RType> RType rin(RType &r) {
int b = 1, c = getchar(); r = 0;
while (c < '0' || '9' < c) b = c == '-' ? -1 : b, c = getchar();
while ('0' <= c && c <= '9') r = r * 10 + (c ^ '0'), c = getchar();
return r *= b;
}
int main() {
rin(n), rin(valx);
for (int i = 1; i <= n; ++i) {
rin(vald[i]);
OPERON(vald[i], valx, sub);
}
for (int i = 2; i <= n; ++i) {
int fa, tofa, tou;
rin(fa), rin(tofa), rin(tou);
OPERON(tofa, valx, sub), OPERON(tou, valx, sub);
gr.addEdge(i, fa, tofa);
gr.addEdge(fa, i, tou);
} dfs(1, 0);
int ans = add(f[1][0][0], f[1][0][1]);
if (n & 1) ans = sub(0, ans);
printf("%d\n", ans);
return 0;
}

THE END

Thanks for reading!

我偏要 让世界都坠落
湮灭前 整个银河繁星闪烁
撕裂哀鸣是最后的挽歌

——《恒星坠落之时(森罗万象)》 By 星尘/赤羽

> Link 恒星坠落之时 - Bilibili

「SOL」行列式 (模拟赛)的更多相关文章

  1. 「NOWCODER」CSP-S模拟赛第3场

    「NOWCODER」CSP模拟赛第3场 T1 货物收集 题目 考场思路即正解 T2 货物分组 题目 考场思路 题解 60pts 算法:一维 DP 100pts 算法:一维 DP ?线段树 + 单调栈 ...

  2. 「 题解」NOIP2021模拟赛(2021-07-19)

    小兔的话 欢迎大家在评论区留言哦~ D - 矩阵 简单题意 一个 \(i * i\) 的 \(01\) 矩阵,若满足 每一行 和 每一列 都满足 恰好 有 \(2\) 个位置是 \(1\) 时,称为 ...

  3. Solution -「牛客 NOIP 模拟赛」打拳

    \(\mathcal{Description}\)   现 \(2^n\) 个人进行淘汰赛,他们的战力为 \(1\sim 2^n\),战力强者能战胜战力弱者,但是战力在集合 \(\{a_m\}\) 里 ...

  4. 「数据结构」:模拟指针(simulated pointer)

    模拟指针,也就是清华严老师<数据结构-C语言描述>中的静态链表,静态链表的引用是使用一段连续的存储区还模拟指针的功能,可以有效的利用一段连续内存进行一定范围内可变的子链表的空间分配,此数据 ...

  5. 「题解」NOIP模拟测试题解乱写II(36)

    毕竟考得太频繁了于是不可能每次考试都写题解.(我解释个什么劲啊又没有人看) 甚至有的题目都没有改掉.跑过来写题解一方面是总结,另一方面也是放松了. NOIP模拟测试36 T1字符 这题我完全懵逼了.就 ...

  6. 「题解」NOIP模拟测试题解乱写I(29-31)

    NOIP模拟29(B) T1爬山 简单题,赛时找到了$O(1)$查询的规律于是切了. 从倍增LCA那里借鉴了一点东西:先将a.b抬到同一高度,然后再一起往上爬.所用的步数$×2$就是了. 抬升到同一高 ...

  7. 「考试」联赛模拟36-39,noip晚间小测2-3

    36.1 party(CF623D) 很是鸡贼的一道题 首先要明确一点,抓人是有策略,而不是随机的,可以认为等同于按一个给定的顺序猜人,那么这时猜中的概率就只是抓住这个人的概率了 对于每一次猜测,因为 ...

  8. 「考试」noip模拟9,11,13

    9.1 辣鸡 可以把答案分成 每个矩形内部连线 和 矩形之间的连线 两部分 前半部分即为\(2(w-1)(h-1)\),后半部分可以模拟求(就是讨论四种相邻的情况) 如果\(n^2\)选择暴力模拟是有 ...

  9. 冲刺$\mathfrak{CSP-S}$集训模拟赛总结

    开坑.手懒并不想继续一场考试一篇文. 既没必要也没时间侧边栏的最新随笔题解反思相间也丑 而且最近越来越懒了竟然都不写题解了……开坑也是为了督促自己写题解. 并不想长篇大论.简要题解也得写啊QAQ. 目 ...

  10. NOIP2018 模拟赛(二十二)雅礼NOI

    Preface 这次的题目都是NOI+的题,所以大家的分数都有点惨烈. 依靠T1大力骗分水到Rank2 所以想看正解的话看这里吧 A. 「雅礼NOI2018模拟赛(一) Day1」树 看一眼题目感觉十 ...

随机推荐

  1. supervisor管理java进程

    安装 yum install supervisor 设置开机启动 systemctl enable supervisord 启动supervisord systemctl start supervis ...

  2. UBUNTU安装代码阅读器Understand

    https://blog.csdn.net/weixin_40641902/article/details/79607225 1.直接下载 Understand-3.1.670-Linux-64bit ...

  3. 2021级《JAVA语言程序设计》上机考试试题2

    以下是学生页面 首先先给上数据库 在准备准备工作 以下为代码: package Bean; public class Student { private String StuID; private S ...

  4. Spring依赖注入问题

    记录一下最近复习的Spirng依赖注入问题 主要介绍两个注入方式 也是用的最多的 1.构造方法注入 2.Setter注入. 参考文档:https://blog.csdn.net/weixin_5541 ...

  5. HDMI转USB视频采集卡(ACASIS 1080P高清视频采集卡)--九五小庞

    ACASIS阿卡西斯是深圳市菲德越科技有限公司旗下数码科技品牌.菲德越是2008年成立的一家专注于采集卡.硬盘盒.集线器等专业3C配件产品,集研发.设计.生产.销售于一体的高新科技公司,我们公司以向客 ...

  6. 从零开始使用阿里云OSS服务(白嫖)

    阿里云对象存储服务OSS使用记录 1 新人免费开通OSS服务 访问 阿里云官网,登录账号(个人新用户账号),首页搜索 对象存储OSS,点击下方的直达. 点击立即开通,此时会在一个新网页中完成开通 完成 ...

  7. .net core Ubuntu下docker部署(精简版)

    一.首先先添加一个.netCore 项目,选择启用docker(我用的是vs2019,之前的可能需要手动点击项目添加docker支持) 没有的话需要手动添加 之后你的项目会存在这个dockerfile ...

  8. 3D场景建模

    在这个虚拟现实的世界里,我们将会和你一起在云端构建属于自己的世界. 这里有一款3D场景编辑器可以让你在上面进行编辑.设计及渲染. 它是一个非常炫酷的软件,在这里,你可以尽情发挥你的想象力,创造出独一无 ...

  9. Android:LitePal 在第一次创建表之后第二次创建新的表不生效

    因为业务需求的增长,后续需要继续创建新的表,有可能代码没有任何报错,同时数据库也没有任何新的表加入进来. 修改 litepal.xml 的 version,如果之前是 1,那么修改为 2,总之比之前 ...

  10. ArcGIS for Android 地图图文查询

    ArcGIS for Android 地图图文查询 1.前期项目准备 1.1. 创建新工程 新建一个空活动项目 选择语言.平台,修改命名等 1.2. 添加ArcGIS SDK build.gradle ...