1. 程式人生 > 其它 >「SOL」行列式 (模擬賽)

「SOL」行列式 (模擬賽)

#NOI模擬賽# 『主要內容:行列式 / 樹形DP』

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

歡迎轉載٩(๑❛ᴗ❛๑)۶,請在轉載文章末尾附上原博文網址~