2025.5.3笔记

如果我有写错或令人不理解的地方,请及时指出,谢谢!!!

DP!!!

image

定状态(没听。。。)

\(fib\) 数列

\[f_i = \begin{cases} 0 & i = 0 \\ 1 & i = 1 \\ f_{i - 1} + f_{i - 2} & i > 1 \end{cases} \]

复杂度 \(\mathcal O(n)\)

通项公式:\(f_i = \dfrac{(\dfrac{1 + \sqrt{5}}{2})^i - (\dfrac{1 - \sqrt{5}}{2})^i}{\sqrt{5}}\)

优化:矩阵快速幂。

复杂度 \(\mathcal O(\log n)\)

。。。还是远古码风。

点击查看代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#define int long long

using namespace std;
const int mod = 1e9 + 7;

int n;
long long k;
struct node
{
  int a[3][3];
  void bb()
  {
    a[1][1] = a[2][2] = 1;
    a[1][2] = a[2][1] = 0;
  }
} m;

node operator*(const node &a, const node &b)
{
  node ret;
  for (int i = 1; i <= 2; ++i)
  {
    for (int j = 1; j <= 2; ++j)
    {
      ret.a[i][j] = 0;
    }
  }
  
  for (int i = 1; i <= 2; ++i)
  {
    for (int k = 1; k <= 2; ++k)
    {
      for (int j = 1; j <= 2; ++j)
      {
        ret.a[i][j] += 1ll * a.a[i][k] * b.a[k][j] % mod;
        ret.a[i][j] %= mod;
      }
    }
  }
  return ret;
}

node ksm(node a, long long k)
{
  node ret;
  ret.bb();
  for (; k; k >>= 1, a = a * a)
    if (k & 1)
      ret = ret * a;
  return ret;
}
node ret;

signed main()
{
  memset(m.a, 0, sizeof (m.a));
  cin >> n;
  if (n == 1)
  {
    cout << 1 << '\n';
    return 0;
  }
  if (n == 2)
  {
    cout << 1 << '\n';
    return 0;
  }
  m.a[1][1] = m.a[1][2] = m.a[2][1] = 1;
  m.a[2][2] = 0;
  n--;
  ret = ksm(m, n - 1);
  cout << (ret.a[1][1] + ret.a[1][2]) % mod;
  return 0;
}

注意:矩阵乘法的循环把 \(j\) 放在最后是最快的。

原理:

image

image

最后枚举 \(j\) 很多次都在同一行读取。

读取速度能差 \(100\) 倍。

从小到大枚举比从大到小枚举快。

矩阵乘法优化

image

\(f\)\([\)走了几步\(]\)\([\)位置\(]\) \(=\) 方案数

\(f[i][j] = \displaystyle\sum^n_{k = 1}f[i - 1][k]\cdot M[k][j]\)

\(t\leq 10^9\)

一旦看到转移式子长成这样,就是矩阵乘法。

1.升维:\(f[i][1][j] = \displaystyle\sum^n_{k = 1}f[i - 1][1][k]\cdot M[k][j]\)
2.降维:把 \(f_i\)\(f_{i - 1}\) 当成两个二维数组。

\(f_i[1][j] = \displaystyle\sum^n_{k = 1}f_{i - 1}[1][k]\cdot M[k][j]\)

就有了矩阵乘法的形式, \(f_i = f_{i - 1}\cdot M\)

\(f_t = f_0\cdot M^t\)\(f[0][1] = 1 \to f_0[1][1] = 1\)

答案即为 \(f_t.a[1][n]\)

这类 DP 的特征:
1.从 \(i - 1\) 转移到 \(i\)
2.转移系数 \(M\)\(i\) 无关。

例1

Luogu 迷路

边有长度,因此拆边,转化成上一个问题。

但会有 \(700\) 多个点,会超时。

image

让它们共享几条边。

这样每一个点只会伸出最多 \(90\) 个点。

点击查看代码
#include <iostream>
#include <cstring>

using std::cin;
using std::cout;
const int N = 110;
const int mod = 2009;

struct Matrix
{
	int a, b;
	int c[N][N];
	void init(int x = 0, int y = 0)
	{
		a = x, b = y;
		memset(c, 0, sizeof (c));
	}
	void init_i(int x)
	{
		init(x, x);
		a = b = x;
		for (int i = 1; i <= x; ++i)
			c[i][i] = 1;
	}
	friend Matrix operator*(const Matrix &a, const Matrix &b)
	{
		Matrix ret;
		ret.init(a.a, b.b);
		for (int i = 1; i <= a.a; ++i)
			for (int k = 1; k <= b.b; ++k)
				for (int j = 1; j <= b.a; ++j)
					ret.c[i][k] = (1ll * a.c[i][j] * b.c[j][k] + ret.c[i][k]) % mod;
		return ret;
	}
} g;

Matrix ksm(Matrix a, int k)
{
	Matrix ret;
	ret.init_i(a.a);
	for (; k; a = a * a, k >>= 1)
		if (k & 1)
			ret = ret * a;
	return ret;
}

int main()
{
	int n, t;
	cin >> n >> t;
	char c[20];
	g.init(9 * (n - 1) + 9, 9 * (n - 1) + 9);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= 8; ++j)
			g.c[(i - 1) * 9 + j][(i - 1) * 9 + j + 1] = 1;
	for (int i = 1; i <= n; ++i)
	{
		cin >> (c + 1);
		for (int j = 1; j <= n; ++j)
		{
			if (c[j] > '0')
				g.c[(i - 1) * 9 + c[j] - '0'][(j - 1) * 9 + 1] = 1;
		}
	}
	g = ksm(g, t);
	cout << g.c[1][(n - 1) * 9 + 1] << '\n';
	return 0;
}

例2

在例1上面的基础上令 \(M[i][j] = \displaystyle\sum^k_{r = 1}in[i][r] \cdot out[j][r]\)

其中 \(k\leq 20\)\(n\leq 1000\)

我们在输入的时候把 \(out[k][j]\) 处理成 \(out[j][k]\).

于是就变成了 \(M[i][j] = in[i][j] \cdot out[j][r] = in\cdot out\)

显然直接 \(\mathcal O(n^3\log n)\) 的朴素矩阵乘法是不可以的。

于是 \(f_t = f_0\times M^t = f_0 \times in \times out \times in\times \dots \times out = f_0 \times in \times (in\cdot out)^{t - 1} \times out\)

\(in\cdot out\) 得到的就是 \(k\times k\) 的矩阵了,就可以 \(\mathcal O(k^3\log t)\) 了。

排列DP

排列定义:\(1\sim n\) 中的每一个数都只出现一次的序列。

\(1\sim n\)\(n!\) 种排列。

一般问题:\(n!\)种排列中有多少个满足条件的排列。

第一维 \(f[i]\) 表示已经插入了 \(1\sim i\)\(i\sim n\)

例3

\(1\sim n\)\(n\) 个数的排列有多少个排列有偶数个逆序对

变化的东西:逆序对数量、方案数。

\(f[i][j]\) 表示已经插入了 \(1\sim i\),有 \(j\) 个逆序对的方案数。

要把 \(i + 1\) 插到 \(k\)。由于是从小到大插入的,所以它前面的都比它小,后面的都比它大,所以会增加 \(i - k\) 个逆序对。

\(f[i][j]\to f[i + 1]f[i - k + j]\)

朴素:

点击查看代码
f[0][0] = 1;
	for (int i = 0; i < n; ++i)
		for (int j = 0; j <= i * (i - 1) / 2; ++j)
			for (int k = 0; k <= i; ++k)
				f[i + 1][j + i - k] += f[i][j];

可是我们只关注奇偶性,于是:

点击查看代码
#include <iostream>

int main()
{

	f[0][0] = 1;
	for (int i = 0; i < n; ++i)
		for (int j = 0; j <= 1; ++j)
			for (int k = 0; k <= i; ++k)
				f[i + 1][(j + i - k) % 2] += f[i][j];

	return 0;
}

优化到 \(\mathcal O(n^2)\) 了。

但是我们可以算出 \(1\sim i\) 有几个奇数、偶数,这样就优化到 \(\mathcal O(n)\) 了。

但这实际上是一个数学题,结论是 \(\dfrac{n!}{2}\)

例4

image

\(f[i][j]\) 表示从 \(i\sim n\) 插入到 \(i\),有 \(j\) 个数满足条件的方案数。

这样,\(i - 1\) 无论插入到哪,满足条件的个数都不会减小。

但如果 \(i - 1\) 插入到了最前面,那它也会满足条件,所以个数会 \(+1\)

所以

\[\begin{cases} f[i - 1][j + 1] += f[i][j] \\ f[i - 1][j] += f[i][j]\times (n - i + 1) \end{cases} \]

例5

image

不超过 \(2\),可以分为 \(1\)\(2\)\(1\) 显然只有一种。

对于 \(2\)\(f[i][j]\) 表示选到第 \(i\) 个,所有的最长上升子序列的第二项最小坐标为 \(j\) 的方案数。

考虑插到 \(k\),如果 \(k = 0\),也就是插到最前面,最小坐标会往后移一位;否则,它会变成最小的坐标,所以

\[\begin{cases} f[i + 1][j + 1] += f[i][j] \\ f[i + 1][k + 1] += f[i][j] \end{cases} \]

为什么是 \(k + 1\) 呢?因为插到第 \(i\) 个数后面,它的坐标是 \(i + 1\)

区间 DP

对相邻的数进行运算。

例6

Luogu 石子合并

第一类区间 DP。

枚举断点。

点击查看代码
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;
const int maxn = 110;

int a[maxn];
int sum[maxn << 1];
int dp[maxn << 1][maxn << 1];

int main()
{
	memset(dp, 0x3f, sizeof (dp));
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i)
	{
		cin >> a[i];
		sum[i] = sum[i - 1] + a[i];
	}
	for (int i = n + 1; i <= n + n; ++i)
	{
		sum[i] = sum[i - 1] + a[i - n];
	}
	for (int i = 1; i <= n + n; ++i)
	{
		dp[i][i] = 0;
	}
	for (int len = 2; len <= n; ++len)
	{
		for (int l = 1, r = len; r <= n + n; ++l, ++r)
		{
			for (int k = l; k < r; ++k)
			{
				dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + sum[r] - sum[l - 1]);
			}
		}
	}
	int ans = dp[1][n];
	for (int i = 1; i <= n; ++i)
	{
		ans = min(ans, dp[i][i + n - 1]);
	}
	cout << ans << '\n';
	memset(dp, 0, sizeof (dp));
	for (int i = 1; i <= n + n; ++i)
	{
		dp[i][i] = 0;
	}
	for (int len = 2; len <= n; ++len)
	{
		for (int l = 1, r = len; r <= n + n; ++l, ++r)
		{
			for (int k = l; k < r; ++k)
			{
				dp[l][r] = max(dp[l][r], dp[l][k] + dp[k + 1][r] + sum[r] - sum[l - 1]);
			}
		}
	}
	ans = dp[1][n];
	for (int i = 1; i <= n; ++i)
	{
		ans = max(ans, dp[i][i + n - 1]);
	}
	cout << ans << '\n';
	return 0;
}

例7

Luogu 能量项链

同上。

点击查看代码
#include <iostream>
#include <vector>
#include <queue>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 110;

int n;
int a[N << 1];
int l[N << 1], r[N << 1];
int f[N << 1][N << 1];

int calc(int i, int k, int j)
{
  return l[i] * r[k] * r[j];
}

int main()
{
  int n;
  cin >> n;
  for (int i = 1; i <= n; ++i)
    cin >> a[i];
  for (int i = 1; i < n; ++i)
    l[i] = a[i], r[i] = a[i + 1];
  l[n] = a[n];
  r[n] = a[1];
  for (int i = n + 1; i <= 2 * n; ++i)
    l[i] = l[i - n], r[i] = r[i - n];
  for (int len = 2; len <= n; ++len)
    for (int i = 1, j = len; j <= 2 * n; ++j, ++i)
      for (int k = i; k < j; ++k)
        f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + calc(i, k, j));
  int ans = -1;
  for (int i = 1; i <= n; ++i)
    ans = max(ans, f[i][i + n - 1]);
  cout << ans << '\n';
  return 0;
}

例8

image

\(f[l][r]\) 表示删完了 \(l\sim r\)\(l,r\) 不删)的最小总代价。

因此 \(f[l][r] = \displaystyle\min_{l < k < r}\{f[l][k] + f[k][r] + a_l\cdot a_r\cdot a_k\}\)

答案即为 \(f[1][n]\)

要保证状态和转移一致。

例9

求区间回文子序列个数。

是第二类区间 DP。

\(f[l][r]\) 表示 \(l\sim r\) 有多少个回文子序列。

转移:\(f[l][r] = f[l + 1][r] + f[l][r - 1] - f[l + 1][r - 1] + (s[l] == s[r])(f[l + 1][r - 1] + 1)\)

例10

image

不能正常的枚举格子,算最小经过的被吓格子。

因为是老鼠数量,所以可能被一个老鼠吓好多次。

如果一个格子被老鼠吓到了第一次,那它最多走 \(2\) 步会再次被吓到。

所以再开两维记录上两步是怎么走的。

\(f[i][j][0/1][0/1]\) 表示走到了 \((i,j)\),上两步分别走的左/右、左/右,所经过的最少被吓格子。

例11

Luogu Cow Relays G

我们只需要魔改一下矩阵乘法,

把乘法改成类似于 Floyd 的更新答案。

每一次更新答案,就相当于多走了一条边。

因为 \(u,v\leq 10^3\),所以直接做是会炸的。

又注意到边数 \(\leq\) \(100\),所以点数 \(\leq\) \(99\)

所以离散化一下就行了。

点击查看代码
#include <iostream>
#include <cstring>

using std::cin;
using std::cout;
const int N = 110;
const int K = 1010;

struct Matrix
{
	int a, b;
	int c[N][N];
	void init(int x = 0, int y = 0)
	{
		a = x, b = y;
		memset(c, 0x3f, sizeof(c));
	}
	void init_i(int x)
	{
		init(x, x);
		a = b = x;
		for (int i = 1; i <= x; ++i)
			c[i][i] = 0;
	}
	friend Matrix operator*(const Matrix &a, const Matrix &b)
	{
		Matrix ret;
		ret.init(a.a, b.b);
		for (int i = 1; i <= a.a; ++i)
			for (int j = 1; j <= b.b; ++j)
				for (int k = 1; k <= a.b; ++k)
					ret.c[i][j] = std::min(ret.c[i][j], a.c[i][k] + b.c[k][j]);
		return ret;
	}
} g;

int id[K];

Matrix ksm(Matrix a, int k)
{
	Matrix ret;
	ret.init_i(a.a); // 初始化为单位矩阵
	for (; k; a = a * a, k >>= 1)
		if (k & 1)
			ret = ret * a;
	return ret;
}

int main()
{
	g.init();
	int n = 0;
	int l, t, s, e;
	cin >> l >> t >> s >> e;
	for (int i = 1; i <= t; ++i)
	{
		int w, u, v;
		cin >> w >> u >> v;
		u = id[u] ? id[u] : id[u] = ++n;
		v = id[v] ? id[v] : id[v] = ++n;
		g.c[u][v] = g.c[v][u] = std::min(g.c[u][v], w);
	}
	g.a = g.b = n; // 设置矩阵维度
	Matrix ans = ksm(g, l);
	s = id[s];
	e = id[e];
	cout << ans.c[s][e] << '\n';
	return 0;
}

例12

Luogu 刷题比赛

这道题显然比较恶心。。。

转移矩阵:
image

而且这道题可能爆 long long,于是我们使用龟速乘。

虽然比较慢,但很好地解决了爆 long long 的问题。

点击查看代码
#include <iostream>
#include <vector>
#include <queue>
#include <cstdio>
#include <stack>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <set>
#include <map>
#include <cstring>
#include <algorithm>
#include <string>
#include <cstring>
#include <climits>
#include <assert.h>
#define dbg cout << "dbg"

using namespace std;
typedef long long ll;
const int N = 20;

struct Matrix
{
  int a, b;
  ll c[N][N];
  void rs()
  {
    memset(c, 0, sizeof(c));
  }
  void rsi()
  {
    rs();
    if (a != b)
      while (1)
      {
      }
    else
      for (int i = 1; i <= a; ++i)
        for (int j = 1; j <= b; ++j)
          if (i == j)
            c[i][j] = 1;
          else
            c[i][j] = 0;
  }
};

ll n, m;
ll p, q, r, t, u, v, w, x, y, z;

ll tc(ll a, ll b)
{
  ll ret = 0;
  for (; b; a = (a + a) % m, b >>= 1)
    if (b & 1)
      ret = (ret + a) % m;
  return ret % m;
}
Matrix operator*(const Matrix &a, const Matrix &b)
{
  Matrix ret;
  ret.rs();
  ret.a = a.a;
  ret.b = b.b;
  for (int i = 1; i <= ret.a; ++i)
    for (int j = 1; j <= a.b; ++j)
      for (int k = 1; k <= ret.b; ++k)
      {
        ret.c[i][k] = (ret.c[i][k] + tc(a.c[i][j], b.c[j][k])) % m;
        // cout << b.c[j][k] <<'\n';
        // dbg;
        // cout << i << ' ' << j << ' ' << k << '\n';
      }
  return ret;
}
void mo(Matrix &a)
{
  for (int i = 1; i <= a.a; ++i)
    for (int j = 1; j <= a.b; ++j)
      a.c[i][j] %= m;
}
Matrix ksm(Matrix a, ll b)
{
  Matrix ret;
  ret.a = ret.b = a.a;
  ret.rsi();
  for (; b; a = a * a, b >>= 1)
  {
    // dbg;
    if (b & 1)
      ret = ret * a;
  }
  return ret;
}

int main()
{
  cin >> n >> m;
  cin >> p >> q >> r >> t >> u >> v >> w >> x >> y >> z;
  Matrix l;
  l.rs();
  l.a = l.b = 11;
  l.c[1][1] = p;
  l.c[1][2] = 1;
  l.c[1][3] = 1;
  l.c[1][4] = q;
  l.c[1][7] = r;
  l.c[1][8] = t;
  l.c[1][11] = 1;
  l.c[2][1] = 1;
  l.c[2][2] = u;
  l.c[2][3] = 1;
  l.c[2][5] = v;
  l.c[2][9] = 1;
  l.c[3][1] = 1;
  l.c[3][2] = 1;
  l.c[3][3] = x;
  l.c[3][6] = y;
  l.c[3][8] = 1;
  l.c[3][10] = 1;
  l.c[3][11] = 2;
  l.c[4][1] = 1;
  l.c[5][2] = 1;
  l.c[6][3] = 1;
  l.c[7][7] = 1;
  l.c[7][8] = 2;
  l.c[7][11] = 1;
  l.c[8][8] = 1;
  l.c[8][11] = 1;
  l.c[9][9] = w;
  l.c[10][10] = z;
  l.c[11][11] = 1;

  mo(l);
  Matrix _;
  _.rs();
  _.a = 11; 
  _.b = 1;
  _.c[1][1] = _.c[2][1] = _.c[3][1] = 3;
  _.c[4][1] = _.c[5][1] = _.c[6][1] = 1;
  _.c[7][1] = _.c[8][1] = _.c[11][1] = 1;
  _.c[9][1] = w;
  _.c[10][1] = z;
  _.c[11][1] = 1;
  mo(_);
  // cout << n - 2;
  Matrix res = ksm(l, n - 2) * _;
  mo(res);
  cout << "nodgd " << res.c[1][1] << '\n'
       << "Ciocio " << res.c[2][1] << '\n'
       << "Nicole " << res.c[3][1] << '\n';

  return 0;
}

ryf大佬的题单

posted @ 2025-05-03 11:25  SigmaToT  阅读(55)  评论(0)    收藏  举报