【XSY3948】行列式(行列式,树形dp)
题面
题解
马神说:这可能是本场比赛最简单的一道题。
黑人问号脸.jpg
(这篇题解我很多地方写得很简略或很不严谨,所以如果有些地方看不懂请自己推一推)
考虑构造矩阵 \(B=(x)_{n\times n}\),然后设矩阵 \(C=A-B\)。
那么矩阵 \(C\) 满足 \(C_{p_i,i}=b_i-x\),\(C_{i,p_i}=c_i-x\),\(C_{i,i}=d_i-x\),且其他地方的值都是 \(0\)。
然后我们要求的是 \(\det(A)=\det(B+C)\),考虑将其线性展开
不会线性展开的可以利用下面的引理自己展开:
引理:
\[\begin{vmatrix} a_{1,1}&a_{1,2}&\cdots&a_{1,n}\\ \vdots&\vdots&&\vdots\\ a_{i-1,1}&a_{i-1,2}&\cdots&a_{i-1,n}\\ b_1+c_1&b_2+c_2&\cdots&b_n+c_n\\ a_{i+1,1}&a_{i+1,2}&\cdots&a_{i+1,n}\\ \vdots&\vdots&&\vdots\\ a_{n,1}&a_{n,2}&\cdots&a_{n,n}\\ \end{vmatrix} =\begin{vmatrix} a_{1,1}&a_{1,2}&\cdots&a_{1,n}\\ \vdots&\vdots&&\vdots\\ a_{i-1,1}&a_{i-1,2}&\cdots&a_{i-1,n}\\ b_1&b_2&\cdots&b_n\\ a_{i+1,1}&a_{i+1,2}&\cdots&a_{i+1,n}\\ \vdots&\vdots&&\vdots\\ a_{n,1}&a_{n,2}&\cdots&a_{n,n}\\ \end{vmatrix} +\begin{vmatrix} a_{1,1}&a_{1,2}&\cdots&a_{1,n}\\ \vdots&\vdots&&\vdots\\ a_{i-1,1}&a_{i-1,2}&\cdots&a_{i-1,n}\\ c_1&c_2&\cdots&c_n\\ a_{i+1,1}&a_{i+1,2}&\cdots&a_{i+1,n}\\ \vdots&\vdots&&\vdots\\ a_{n,1}&a_{n,2}&\cdots&a_{n,n}\\ \end{vmatrix} \]证明:根据行列式的定义用乘法分配律即可简单证明。
展开后,就能写成每行是 \(B\) 或 \(C\) 对应行之一的 \(2^n\) 个矩阵的 \(\det\) 的和。发现这 \(2^n\) 个矩阵中,那些选了大于一行 \(B\) 的矩阵肯定是没有贡献的,因为 \(B\) 是个全 \(x\) 矩阵,而我们又知道 “如果矩阵中有两行成比例,那么其行列式为 \(0\)”。
所以要计算的矩阵中要么选了恰好一行 \(B\),要么没有选 \(B\)。
先考虑没有选 \(B\) 的情况,此时我们要计算的是 \(\det(C)=\sum\limits_{pp}(-1)^{\tau(pp_1,pp_2,\cdots,pp_n)}C_{1,pp_1}C_{2,pp_2}\cdots C_{n,pp_n}\)。(这里用 \(pp\) 只是为了和 \(p\) 区分开来)
此时发现难以直接计算,就有一种比较神奇的做法:
注意到条件 \(1\leq p_i<i\),那么如果把 \(p_i\) 当做 \(i\) 的父亲的话,就会形成一棵树。
由于行列式的定义中,我们每一行都要选一个位置乘起来,不妨设第 \(i\) 行选了 \(C_{i,j}\)(只考虑 \(C_{i,j}\neq 0\) 的情况)。
那么我们不妨把选了 \(C_{i,j}\) 看做选了边 \((i,j)\),发现选的边要么在树上,要么是树上某个点的自环。
那么我们不妨给树上的边 \((i,p_i)\) 加上边权 \(c_i-x\),边 \((p_i,i)\) 加上边权 \(b_i-x\),同时再给树上的每一个点加上一条自环边,其边权为 \(d_i-x\)。(不妨仍称这个带有自环的图叫做 “树”)
那这样选了 \(C_{i,j}\) 就相当于选了 \((i,j)\) 的边权了。
同时,行列式选完位置后,要保证每一行、每一列都恰好只选了一个位置。
对应到树上,就代表树上选完边并对选的边统计每个点入度出度后(自环 \((u,u)\) 对 \(u\) 的入度和出度各贡献 \(1\)),每个点的入度和出度恰好为 \(1\)。
这个时候会发现一个东西:如果我们选了一条边 \((i,j)\)(\(i\neq j\),即不考虑自环),那么我们一定也会选择边 \((j,i)\)。也就是说我们选边时一定是 \((i,p_i)\) 和 \((p_i,i)\) 捆绑着同时选。
证明比较简单,从树的叶子节点层一直往上推就行了。
那这样就很好讨论了,直接树形dp即可。
具体详见代码。
有一个要注意的地方,就是如何确定每一种选择方案前面的系数(\(-1\) 还是 \(1\))。
假设我们选了 \(k\) 组捆绑的边,那么系数就是 \((-1)^k\)。
原因(写得你可能看不懂):我们会把 \((i,p_i)\) 和 \((p_i,i)\) 同时选,而这两个点在矩阵上是关于矩阵斜右向下的对角线对称的。不妨设我们选了一个点 \(u=(x,y)\),那么 \(u\) 的对称点 \(u'\) 肯定也被选了。画画图可以得知,如果我们选的两个点 \(u\)、\(v\)(\(v\neq u'\))构成逆序对,那么它们的对称点 \(u'\)、\(v'\) 也会构成逆序对,此时 \(-1\) 就抵消了。所以只有 \((u,u')\) 构成逆序对时才会有贡献,而 \((u,u')\) 的数量就是我们选的捆绑的边的组数。
那么既然系数和选的边的组数有关,那么系数就可以在树形dp时顺便处理了。
而对于求选了恰好一行 \(B\) 的矩阵的行列式,假设选的是第 \(i\) 行,我们就把原来树上所有 \(i\) 的出边先去掉,再向所有点都连一条边权为 \(x\) 的边,然后也是要满足每个点的入度和出度恰好为 \(1\),要求不同选边方案的贡献的总和。
这个我们同样也可以用树形dp来搞定。
至于这时如何确定每一种选择方案前面的系数(\(-1\) 还是 \(1\)),我们先称选的那条边权为 \(x\) 的边所在的环为 “\(X\) 环”。
不妨设我们在 \(X\) 环上选了边 \((a_1,a_2),(a_2,a_3),\cdots,(a_{k-1},a_k),(a_k,a_1)\),在 \(X\) 环外选了边 \((b_1,c_1),(c_1,b_1),(b_2,c_2),(c_2,b_2),\cdots,(b_l,c_l),(c_l,b_l)\),那么我们要求的系数即为:
我们只需讨论 \(\tau(b_1,c_1,b_2,c_2,\cdots,b_l,c_l,a_1,a_2,\cdots,a_{k-1},a_k)+\tau(c_1,b_1,c_2,b_2,\cdots,c_l,b_l,a_2,a_3,\cdots,a_k,a_1)\) 的奇偶性即可。
记序列 \(p_1=(b_1,c_1,b_2,c_2,\cdots,b_l,c_l,a_1,a_2,\cdots,a_{k-1},a_k)\),将其分为左半部分 \(pl_1=(b_1,c_1,b_2,c_2,\cdots,b_l,c_l)\) 和右半部分 \(pr_1=(a_1,a_2,\cdots,a_{k-1},a_k)\)。
同理得到 \(p_2=(c_1,b_1,c_2,b_2,\cdots,c_l,b_l,a_2,a_3,\cdots,a_k,a_1)\),\(pl_2=(c_1,b_1,c_2,b_2,\cdots,c_l,b_l)\),\(pr_2=(a_2,a_3,\cdots,a_k,a_1)\)。
考虑 \(pl_1\) 中的数 \(p_{1,i}\) 和 \(pr_1\) 中的数 \(p_{1,j}\) 产生的逆序对集合 \(P_1=\{(p_{1,i},p_{1,j})\}\),以及 \(pl_2\) 中的数 \(p_{2,i}\) 和 \(pr_2\) 中的数 \(p_{2,j}\) 产生的逆序对集合 \(P_2=\{(p_{2,i},p_{2,j})\}\),不难发现这两个集合是完全相同的。因为 \(pl_1\) 中的数的集合和 \(pl_2\) 中的数的集合是完全相同的,而 \(pr_1\) 中的数的集合和 \(pr_2\) 中的数的集合也是完全相同的。
所以 \(P_1\) 和 \(P_2\) 对逆序对个数的贡献会因为只考虑奇偶性而被抵消掉。
所以我们只需要考虑 \(pl_1,pr_1,pl_2,pr_2\) 它们各自的逆序对个数之和的奇偶性即可。
\(pl_1\) 和 \(pl_2\) 的贡献直接按我们第一种情况(即求 \(\det(C)\) 的情况)处理即可,而对于求 \(\big(\tau(pr_1)+\tau(pr_2)\big) \bmod 2\):
那么也可以在树形dp时顺便处理了。
代码如下:
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
namespace modular
{
const int mod=1000000007;
inline int add(int x,int y){return x+y>=mod?x+y-mod:x+y;}
inline int dec(int x,int y){return x-y<0?x-y+mod:x-y;}
inline int mul(int x,int y){return 1ll*x*y%mod;}
}using namespace modular;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^'0');
ch=getchar();
}
return x*f;
}
int n,x,d[N];
int cnt,head[N],nxt[N<<1],to[N<<1],bb[N<<1],cc[N<<1];
int f[N][6],g[6];
//不妨设我们选的边权长度为x的边为(u,v),那么下面的“X路径”是指树上的路径v->u(即X环去掉(u,v)的那部分)
//f[u][0]:已处理完u子树内的边,u子树内无X路径,u需要选与父亲相连的那条双向边(即两条单向边都选上)
//f[u][1]:已处理完u子树内的边,u子树内无X路径,u不能选与父亲相连的那条双向边
//f[u][2]:已处理完u子树内的边,u子树包含完整的X路径,u需要选与父亲相连的那条双向边
//f[u][3]:已处理完u子树内的边,u子树包含完整的X路径,u不能选与父亲相连的那条双向边
//f[u][4]:已处理完u子树内的边,X路径的终点在u子树内,起点在u子树外
//f[u][5]:已处理完u子树内的边,X路径的起点在u子树内,终点在u子树外
void adde(int u,int v,int b,int c)
{
to[++cnt]=v;
bb[cnt]=b,cc[cnt]=c;
nxt[cnt]=head[u];
head[u]=cnt;
}
void dfs(int u)
{
f[u][0]=f[u][4]=f[u][5]=1;
f[u][1]=d[u];
for(int i=head[u];i;i=nxt[i])
{
int v=to[i],b=bb[i],c=cc[i];
dfs(v);
memset(g,0,sizeof(g));
g[0]=mul(f[u][0],f[v][1]);
g[1]=mul(f[u][1],f[v][1]);
g[1]=add(g[1],mul(f[u][0],dec(0,mul(mul(b,c),f[v][0]))));
g[2]=mul(f[u][2],f[v][1]);
g[2]=add(g[2],mul(f[u][0],f[v][3]));
g[3]=mul(f[u][3],f[v][1]);
g[3]=add(g[3],mul(f[u][1],f[v][3]));
g[3]=add(g[3],mul(f[u][0],dec(0,mul(mul(b,c),f[v][2]))));
g[3]=add(g[3],mul(f[u][2],dec(0,mul(mul(b,c),f[v][0]))));
g[3]=add(g[3],mul(f[u][5],dec(0,mul(mul(x,b),f[v][4]))));
g[3]=add(g[3],mul(f[u][4],dec(0,mul(mul(x,c),f[v][5]))));
g[4]=mul(f[u][4],f[v][1]);
g[4]=add(g[4],mul(f[u][0],dec(0,mul(b,f[v][4]))));
g[5]=mul(f[u][5],f[v][1]);
g[5]=add(g[5],mul(f[u][0],dec(0,mul(c,f[v][5]))));
memcpy(f[u],g,sizeof(f[u]));
}
f[u][3]=add(f[u][3],mul(x,f[u][0]));//X路径是自环
}
int main()
{
n=read(),x=read();
for(int i=1;i<=n;i++) d[i]=dec(read(),x);
for(int i=2;i<=n;i++)
{
int p=read(),b=dec(read(),x),c=dec(read(),x);
adde(p,i,b,c);
}
dfs(1);
printf("%d\n",add(f[1][1],f[1][3]));
return 0;
}
/*
3 1
1 1 1
1 2 3
1 4 5
*/