?~9.18

做题记录 [8.26,?]

补题list

codeforces 比赛待补

P5770 [JSOI2016] 无界单词

题目描述

  1. 一共有多少个没有border的单词?
  2. 这些没有border的单词中,按字典序排列第 \(K\) 小的单词是哪一个?

做法

1

先考虑第一问。

没有 border 好像不好求。

我们求有 border。

但是一个串有很多 border,我们该对什么计数呢。

考虑最短的border。

有性质,最短的border一定是无界单词。因为只要有更短border,那么这个border一定有border。

并且注意到最短border一定不超过一半,否则中间去掉也是border。

这样我们就能递推了。

\(x_i,y_i\) 分别表示有border和无border。

\(x_i = \sum_{j\le \frac{i}{2}} y_j \times 2^{i-2j}\)

2

字典序最小。

套路贪心。

往里面填然后判当前串的无界单词数量。

一样的递推即可。

P9523 [JOIST 2022] 复制粘贴 3 / Copy and Paste 3

简要题意:

我们可以进行以下的三种操作维护两个字符串 \(X\)\(Y\)

  1. \(X\) 的末尾加入一个字符 \(c\),需要 \(A\) 的代价。
  2. \(Y\) 变成 \(X\),同时将 \(X\) 清空,需要 \(B\) 的代价。
  3. \(X\) 末尾插入一个字符串 \(Y\),需要 \(C\) 的代价。

做法

先手玩。

我们怎么操作?能获得什么?

'aabf[cut]' eodk(aabf)weo(aabf)cvnd(aabf)df[cut](aabf...df)clkd(aabf...df)vope(aabf..df)

而且容易注意到 \(X,Y\) 都一定是 \(S\) 的子串。

不然的话没用一定会被覆盖。

\(2\) 操作一定有 \(3\) 操作。

没有 \(23\) 操作,简单。

有的话,可能是这样:

afdi[...]dfhv[...]faoi[...]weohr[...]dfa

\(f[l,r]\) 表示造出 \([l,r]\) 的代价。

容易转移:

\(f[l,r] \leftarrow f[l+1,r]+A\)

$f[l,r] \leftarrow f[l,r-1]+A $

排除了这两种转移,只剩下形如:

[...]djfioad[...]axvbsdf[...]vposdf[...]

转移 \(f[l,r] \leftarrow f[l,m] + B + k\times C + A\times((r-l+1)-k\times(m-l+1))\)

这个转移似乎不太好做。

trick:倒着转移

正着转移不好做,我们考虑倒着转移。

对于 \(f[l,r]\) ,它下一个出现不交的位置 \([l',r']\),转移到 \(f[l,r']\).

而下一个出现不交的位置最多 \(\frac{n}{r-l+1}\),这是一个调和级数的形式。

于是我们在 \(n^2 \ln n\) 完成转移。

现在问题也就转化为了怎么找相同串的位置?

固定 \(l\)

然后我们对 \(s[l:]\)\(z-algorithm\).

\(z[i]\ge r-l+1\) 的位置就是相同串的位置。

转化为 \(j > i,z_j >i\) 的第一个 \(j\)

跑一个单调栈可以解决。

做完了。

int go[N][N];
ll f[N][N];

vct<int> solve(string str)
{
	int n = sz(str);
	vct<int>z (n + 2);
	int l = -1,r = -1;
	
	// return vct<int>();
	for (int i = 1; i < n;i ++ )
	{
		if (r >= i) z[i] = min(z[i - l], r - i + 1);
		while(i+z[i]<n&&str[i + z[i]] == str[z[i]] )z[i] ++ ;
		if (i + z[i] - 1 > r) r = i + z[i] - 1,l = i;
	}
	z[0] = sz(s);
	vct<int> sta(n + 2);
	vct<int> ans(n);
	int hh = 1,tt = 0,res = n + 1;
	for (int i = n - 1;i >= 0;i -- )
	{
		for (int j = hh;j <= tt;j ++ )
		{
			if (z[ sta[j] ] >i) res = sta[j];
			else {
				hh = j;break;
			} 
		}
		ans[i] =res;
		while (hh<=tt&&z[sta[tt]] < z[i]) tt--;
		sta[++tt] = i;
	}
	return ans;
}

void solvemain()
{
	cin >> n >> s >> A >> B>>C;
	s = ' ' + s;
	
	for (int l = 1;l <= n;l ++ )
	{
		auto ans = solve(s.substr(l));
		for (int r = l ;r <= n;r++)
		{
			go[l][r] = ans[r-l] + l;
		}	
	}
	mem(f,0x3f);
	auto chk =[&](ll &x,ll y)
	{
		if (y < x) x = y;
	};
	rep(l,1,n) f[l][l] = A;
	for (int len = 1;len <= n;len ++ ){
		for (int l = 1;l + len - 1 <= n;l ++ )
		{
			const int r = l + len - 1;
			if (f[l][r] >= 1e18) continue;
			chk(f[l][r +1],f[l][r] + A);
			chk(f[l-1][r],f[l][r] + A);
			
			int L = go[l][r],R = L + len - 1,cnt = 2;
			while (R <= n)
			{
				chk(f[l][R],f[l][r] + cnt * C + B + ((R-l+1)-cnt*(r-l+1))*A );
				L = go[L][R],R = L + len - 1;cnt ++ ;
			}
		}
	}
	cout << f[1][n] << endl;
}

AT_agc036_f [AGC036F] Square Constraints

题目描述

给定一个整数 \(N\)。请计算满足以下条件的 \((0,1,\cdots,2N-1)\) 的一个排列 \((P_0,P_1,\cdots,P_{2N-1})\) 的个数。由于答案可能非常大,请输出其对 \(M\) 取模的结果。

  • 条件:对于所有 \(i\ (0\leq i\leq 2N-1)\),都有 \(N^2 \leq i^2 + P_i^2 \leq (2N)^2\)

做法

容易转化成每个若干个 \(p_i \in [l_i,r_i]\)

这个限制也容易画到图上成一个 \(\frac{1}{4}\) 圆环。

我们发现这个限制一定是 \(r_i,l_i\) 递减。

考虑弱化,若没有下界咋做?

我们直接排序,然后从小到大选即可。

方案数 \(\prod_i (r_i-i)\).

启发我们容斥掉下界。

相当于每次从 \(l_i -1,r_i\) 中选一个。

然后排完序算。

我们可以提前排序。

全部排在一起,然后容易注意到若不选 \(l\)\(r\) 一定待在最后。

LrrrLrLrrLrrLrLLrr | RRRRRRRR

\(r\) 是没有下界的,\([L,R]\) 是一对。

怎么算不选 \(L\)\(R\) 时前面有几个?

考虑提前枚举 \(k\) 表示前 \(m\) 个中选了几个 \(L\)

算一算可以算出。(因为 \(r\) 是必选的。

做一个 dp \(f_{i,j}\) 表示前 \(i\) 个选了 \(j\)\(L\)

答案加上 \(f_{m,k} \times (-1)^k\)

cin>> n>>mod;
vector<array<int,2> > v;
v.pb({0,0});
for (int i = 0 ;i <= 2 * n - 1;i ++ )
{
    int zuo = 0 ;
    if (n * n - i * i > 0)
    {
        zuo = ceil(sqrtl(n * n - i * i) );
    }
    int you = sqrtl(4*n*n - i * i);
    you = min(you,2 * n - 1);
    if (zuo > you) {
        cout << 0 << endl;return;
    }
    zuo -- ;
    v.pb({zuo,you});
}
sort(v.begin() + 1,v.end(),[&](array<int,2> a,array<int,2> b)
{
    if (a[0 ] != -1 && b[0] != -1) return a < b;
    if (a[0] == -1 && b[0] != -1)
    {
        return a[1] <= b[0];
    } 
    if (a[0] != -1 && b[0] == -1)
    {
        return a[0] < b[1];
    }
    return a[1] < b[1];
});
const int m = sz(v) - 1;
vector<int> cntR(m + 2);

for (int i = 1;i <= m ;i ++ )
{
    cntR[i] = cntR[i - 1] + (v[i][0] == -1);
}
auto norm = [&](int &x)
{
    if (x < 0) x += mod;
    if (x >= mod) x-= mod;
};
auto add = [&](int &a,int b)
{
    a += b;
    norm(a );
};
int res = 0 ;
int all = m - cntR[m] + m;
for (int k = 0;k <= m;k ++ )
{
    vct<vct<int> > f(m + 2,vct<int>(m + 2));
    f[0][0] = 1;
    for (int i = 1;i <= m;i ++ )
    {
        int cntl =min(k, i - cntR[i]);
        for (int j = 0;j <= cntl;j ++ )
        {
            if (v[i][0] == -1)
            {
                f[i][j] = 
                    f[i-  1][j]*1ll * (v[i][1]+1-(j + cntR[i] - 1) )  %mod;
            }
            else{
                //l_i
                if (j)
                    add(f[i][j],f[i - 1][j - 1] * 1ll * (
                    v[i][0]+1-(j + cntR[i] - 1)
                )%mod );
                //R_i
                const int gong2 = (m-cntR[m]) - k;
                const int pr2 = i - 1 - cntR[i - 1] - j;


                int hou = gong2 - pr2;
                add(f[i][j],f[i - 1][j] * 1ll * (
                    v[i][1]+1-(m-hou )
                ) % mod);

            }
        }
    }
    add(res,f[m][k] * ((k%2==0)?1:-1));
}
cout<< res<<endl;

CF2252F Spectral Components

题目描述

给定一棵包含 \(n\) 个节点的树。每个节点 \(i\) 被涂上了颜色 \(c_i\)

对树中出现的每种不同颜色 \(c\),记 \(m_c\) 为颜色 \(c\) 的节点总数。还给定一个长度为 \(n\) 的数组 \(k\),其中 \(k_c\)\(1 \le k_c \le m_c\))表示颜色 \(c\) 需要选择的连通块大小。

对于每种颜色 \(c\)(互不影响),你需要选择一个恰好包含 \(k_c\) 个节点的连通子图(连通块)。选中的连通块中的节点不要求全为颜色 \(c\)

所选连通块的代价定义为:树中每一个颜色为 \(c\) 的节点到该连通块的最短距离之和(某个节点 \(v\) 到连通块 \(S\) 的距离定义为 \(v\)\(S\) 中任意节点 \(u\) 的最短路径上的最少边数的最小值)。

对于每种颜色 \(c\)\(1\)\(n\)),输出一个大小为 \(k_c\) 的合法连通块的最小可能代价。如果颜色 \(c\) 在树中没有出现,则输出 \(-1\)

做法

读错题了谴责一下啊。

有点像这个 https://www.luogu.com.cn/problem/CF1824B2

容易想到考虑虚树。

变成在虚树上选个大小为 \(k_c\) 的联通块,使得关键点到块距离之和最小。

怎么考虑若干个关键点到一个点距离最小呢?

我们对一条边考虑 \(cnt_L,cnt_R\) 表示把它删掉后左右分别的关键点个数。

不妨设 \(cnt_L < cnt_R\),我们可以往 \(R\) 移动。

\(\Delta = cnt_R - cnt_L\).

所以我们会一直移动,直到当前点每条出边都是向内指的。

即每棵子树的 \(cnt\)\(\le m - cnt\).

也就是做了一个类似重心的事情。

求得一个带权重心 \(root\)

可以按边来算这个贡献。

\(cost = \sum_{edge} min(cnt_L,cnt_R)\)

接下来我们要继续选。 选成一个块。

注意到块内的边是一定不会走的。

我们的代价变成了:

\(cost - \sum_{S}min(cnt_L,cnt_R)\)

选最大的 \(k-1\) 个即可。

CF2239D Hunting the Beast

题目描述

在浙江省中部的诸暨市,当地民间传说有一种名为“马头熊”的野兽。它栖息在深山之中,会在夜里潜入村庄吞食家畜,并袭击落单的旅人。尽管许多老人都声称曾亲眼见过它,但至今从未有人拍到过这种生物的照片。

一个勇敢的 \(m\) 人的团队决定去山上猎杀这只野兽。这座山上的位置和小径可以被建模为一个功能图 \(G\),其中有 \(n\) 个点(编号为 \(1\)\(n\))。一个功能图是一个 \(n\) 个点、\(n\) 条边的有向图,每个顶点的出度都恰好为 \(1\)。另外,我们还知道这座山的小径没有自环。

这个小组会选择刚好 \(m\) 个不同的点加入它们的初始的起点集合 \(S\)。一个起点集合是成功的当且仅当图中的每个点 \(u\) 都可以从至少一个点 \(v\in S\) 到达(一个点始终可以从它自己到达)。

一共有 \(\binom{n}{m}\) 中方法选择一个大小为 \(m\) 的起点集合。他们定义一个图 \(G\) 的分数为它所拥有的成功的起点集合的个数。

然而,这座山的确切的小径分布是未知的。如果每个点的出边的终点是从剩下的 \(n-1\) 个点(除去它自己)中随机选取的,就会有 \((n-1)^n\) 个可能的功能图。给定 \(n,m\),你的任务是计算所有 \((n-1)^n\) 个可能的功能图的分数之和。由于答案可能非常大,输出它对 \(998\,244\,353\) 取模的值。

做法

直接计数困难。

拆下贡献?

计算所有 \((n-1)^n\) 个可能的基环森林拥有的成功起点集合的个数之和。

不妨考虑每个集合能对应多少个基环森林。

发现由对称性,任意集合都一样。

所以我们不妨钦定选择 \(1\dots m\),答案 \(\times \binom{n}{m}\)

转化为关键点必须覆盖叶子,且每个纯环也必须存在一个。

又容易化为每个非关键点不能当叶子,也不能当一个纯环。

现在似乎可做了。

考虑容斥

钦定非关键点中 \(i\) 个成叶子,\(j\) 个成环。

系数 \((-1)^{(i +j)}\)

选择方案数 :$\binom{n-m}{i +j} \times \binom{i + j}{j} $

非钦定点:\((n-i-j-1)^{(n-i-j)}\)

叶子:\((n-i-j)^{i}\)

\(待补\)

https://codeforces.com/contest/2258/problem/B2

\(n\) 个数,一次操作可以选择 \(x\),令 \(a_i \leftarrow a_i-x,x\).

\([1,k]\) 次操作后,众数出现次数 \(max\)

做法

容易想到枚举众数 \(x\)

手玩容易发现 \(8 \to 44 \to 2222\to 11111111\).

也就是说想要最多操作次数,操作形如:

\[x2^{k-1},x2^{k-2},x2^{k-3},x2^{k-4},\dots x \]

然后我们容易想到去刻画 \(a_i\) 产生了多少 \(x\)

容易发现大多数 \(a_i \to \lfloor \frac{a_i}{x} \rfloor \times x\).

物尽其用了。

但是对于 \(a_i > 2^{k}x\) 他们只能被拆分 \(2^k-1\) 个。

特殊地,\(a_i=2^kx\) 可以拆除 \(2^k\) 个。

并且容易发现当 \(k > 20\) 的时候,可以全部拆成 \(1\)

现在开始计算。

枚举 \(k\)

接下来考虑枚举合法的 \(x\)

要求 \(x\le\frac{m}{2^k}\)

反之,我们可以去掉第一次操作。

这样 我们还剩下一个式子:\(\sum \min(a[i]/x,2^k-1)\)

这个怎么处理呢?

我们可以考虑转化成枚举 \(\sum_{j\le 2^k-1} j\times cnt(jx,(j+1)x]\).

容易发现等价于

\[\sum _{j\le 2^k-1} [xk\le a_i] \]

trick: min的转化

总结一下就是 \(min(a,b)\) 可以转化成

\(\sum_j [j\le a][j \le b]\).

霍尔定理 小记

4. Hall 定理

对于二分图 \(G=(V,E)\),令 \(N(v)\) 表示与点 \(v\) 相邻的点集,则关于最大匹配,我们有如下结论:

Hall 定理:设二分图 \(G\) 的两部分别为 \(X,Y\)\(|X|\leq |Y|\),则其存在一个大小为 \(|X|\) 的匹配当且仅当 \(\forall S\subseteq X\),有 \(|S|\leq |\bigcup\limits_{v\in S} N(v)|\)

证明:

必要性:若存在 \(S\) 使得条件不成立,考虑 \(S\) 中所有点的匹配点,形成了一个大小不小于 \(|S|\) 的点集,而它们必在 \(\bigcup\limits_{v\in S} N(v)\) 中,这说明一个子集的大小大于超集的大小,这是不可能的。

充分性:若条件成立但不存在大小为 \(|X|\) 的匹配,则选出左侧一个非匹配点,从其出发进行尝试增广,设访问到的左部点集合为 \(L\),右部点集合为 \(R\),由于无法成功增广则增广过程中的递归终止点必然在 \(L\) 中,考虑递归树,每个 \(L\) 中的点的父结点为 \(R\) 中的点,则有 \(|L|=|R|+1\),而 \(R\) 应当是 \(L\) 中所有点的 \(N\) 的并,这就与条件矛盾。

Hall 定理有一个简单的推论:

**推论:若一个无向图每个点度数都为 \(k\),则称其为 \(k\) 正则图,那么左右点数相等的 \(k\) 正则二分图必有完美匹配(\(k\ge 1\))。

Hall 定理还可以用于点带权值的情况,若左部点 \(i\) 需要匹配 \(a_i\) 个右部点(可重复),而右部点 \(i\) 可匹配 \(b_i\) 个左部点(可重复),那么我们只需将定理条件中的 \(|S|\)\(|\bigcup\limits_{v\in S} N(v)|\) 分别改成其元素和即可。

我们还可以对此定理进行推广:

Hall 定理推广:设二分图 \(G\) 的两部分别为 \(X,Y\),则其最大匹配为 \(|X|-\max(|S|-|\bigcup\limits_{v\in S} N(v)|)\),其中 \(S\subseteq X\)

https://atcoder.jp/contests/abc424/tasks/abc424_g 例题 hall定理点带权。


trick:去二分化

如果发现一个本来可以用二分的问题,但是用二分会炸或者不优。

可以考虑双指针 / 滑动窗口。

https://codeforces.com/contest/2258/problem/C

一棵树,询问 \(x,y,d\)

返回 \(dist(x,y)\ge d\). 3n 次问出直径。

容易发现想要知道两点距离,可以 \(\log n\) 次询问得到。

但是这个询问次数显然跟 \(log\) 一点关系都没有,而且我们二分只能知道两点的信息。

得到信息其实很少浪费了很多询问。

我们考虑去二分化。

容易联想到双指针,那我们就初始令 \(d=0\). \(x=1\)

若当前 \(d < d(x,y),d:=d+1\)

反之跑 \(y\)

值域维度只会跑一次,点维度由于找两个端点,所以会跑两次。

做完了。

方差小记

定义:\(var(x) = E([x-E(x)]^2)\) .

容易拆开得到 \(var(x) = E(x^2) - E(x)^2\)

平方的期望-期望的平方。

统概里的 \(s^2 = \frac{1}{n} \sum (x_i-x)^2\)

也可看出离散随机变量,是类似的。


方差经典技巧

\[\bar y=\frac{1}{m}\sum_{i=1}^m y_i \]

有恒等式

\[\sum_{i=1}^m(y_i-t)^2 = \sum_{i=1}^m(y_i-\bar y)^2 +m(t-\bar y)^2 \]

因此

\[\boxed{t=\bar y} \]

时平方和最小。

最小值为

\[\boxed{ \min_t\sum_{i=1}^m(y_i-t)^2 = \sum_{i=1}^m(y_i-\bar y)^2 } \]

若方差为

\[s^2=\frac1m\sum_{i=1}^m(y_i-\bar y)^2 \]

\[\boxed{ \min_t\frac1m\sum_{i=1}^m(y_i-t)^2=s^2 } \]

记忆:

\[\sum(y_i-t)^2\Rightarrow t=\text{平均数} \]

\[\sum|y_i-t|\Rightarrow t=\text{中位数} \]

模拟赛 1

B

对于每个点 v,求出字典序第二小的最短路。

做法考虑怎么求字典序最小。

直接按小到大,跑搜索树。

然后我们看此时会有很多横叉边。

现在考虑次小。

那么肯定是在某一时刻走了横叉边。

而且字典序最小。

那我们就 dfs。

每个点最多经过两次。

按边小到大。

在经过的第二次记录答案即可。

C

是个好题。

\(n\)\(x_i,c_i\)。选 \(m\) 个颜色互不相同。

使得 \(\sum _{(i,j)} (x_i-x_j)^2\) 最小。

\(N=5e5\)

看到这个式子,我们容易想到方差?

化一下。

同类项合并。

得到 \(m\sum {x^2} - (\sum x)^2\)

容易联想到 \(var(x) = E(x^2) - E(x)^2\).

这个就是方差 \(/ m^2\)

转化为方差最小。

容易根据方差的式子 \(\sum (x_i-x)^2\)

联想到方差经典 trick .

于是我们对于实数 t考虑 \(\sum (x_i-t)^2\)

选距离 \(t\)\(m\) 小。

对于任意两数,只有在中点时 \(t\) 与他们差的大小关系变化。

于是我们得到 \(O(n^3)\) 做法。

D - Paint the Array

选择 \(m\) 区间设为 \([1,m]\).多次操作得到好区间。

\(a\) 与好区间的最小编辑距离。

做法

我们考虑刻画充要。

倒着做。

可以令 \([1,m] \to ????\)

然后顺着这个 \(????\) 我们还能往外扩展。

整个数组形如:

\(m = 5\)

\(1 2 3 4 523453454534534123412345\)

变化的位置一定有 \(1/m\)

dp套dp 小记

外层 dp 的状态就是内层 dp 的结果

https://www.cnblogs.com/dXqwq/p/14274671.html

模拟赛 2

A:https://ac.nowcoder.com/acm/problem/318521

仔细分析一下不是困难的。

B:https://codeforces.com/gym/106589/problem/J

维护一下 pre suf ans,合并一下。

C:https://newoj.daimayuan.top/p/5599?tid=6a9a88177cd42c871780e950

D:https://www.luogu.com.cn/problem/CF986F

对于问一个集合的数能否拼出... 的问题,且可以任意选任意个。

需要建立直觉跑同余最短路。

\(n\) 个数,最小数为 \(V\)

时间复杂度是 \(O(nV)\).

同余最短路 转圈

https://vjudge.net/contest/846575#problem/H

应该是个好题。

问有多少格子集合,使得经过的 \(1\to n\) 路径集合都相同。

我们直接按照经过路径集合划分等价类。

答案为 \(\sum2^{size}\)

怎么划分呢?

我们考虑哈希。

由于一个路径定为 \(\sum x_i\times p^i\)

而 每一个格子的次幂是固定的。

所以对于一条路径我们可以直接做权值和哈希。

于是我们把所有经过一点的路径哈希值之和加在一起可以得到该点哈希值。

需要多模哈希。

模拟赛 3

A

\(f_{i,j}\)\(i\) 个字符串选 \(j\) 个的最大 \(lcp\)

容易想到 trie 树。

然后容易发现就是每次加上去更新一下 \(f_j\) 即可。

分别考虑有没有 \(i\) 然后更新。

B

你有一个长度为

\[n \]

的序列。其中每个位置包含若干个棋子,棋子可能是红色,黄色,和蓝色中的一种。保证每个位置至少有一个棋子,并且同一个位置上的棋子的颜色两两不同。

你希望把序列分成若干个连续段。一个段是好的,当且仅当这个段里面所有格子的红色棋子个数大于等于黄色棋子,并且红色棋子个数大于等于蓝色棋子个数。

你想知道,分段之后好的段的总长度最长是多少。

\(X_i=\sum_{j=1}^i r_j - y_j, Y_i=\sum_{j=1}^i r_i-b_i\) 。那么 \([l+1,r]\) 这段合法当且仅当 \(X_r \geq X_l, Y_r \geq Y_l\)

直接做只能是一个 cdq 分治。

肯定要利用一些性质。

这里比较显然的性质就是相邻 \((x,y)\) 八相邻。

怎么去考虑这个连续段dp 的优化呢?

我们的想法是连续段的决策可能出现冗余。

决策冗余也就意味着可能有优化空间。

一开始可能会想到最左决策点转移是不是对的?

但是打表发现不对。

于是我们考虑一个连续段什么时候会出现决策冗余。

直接对最终方案中的合法段考虑。

注意到最终方案里的一个合法段,如果 \(X_r > X_l, Y_r > Y_l\),那么可以继续扩大这个区间一定是更优的。所以在最优解中,对于一个合法段,一定有 \(X_r = X_l\) 或者 \(Y_r = Y_l\) 或者 \(l=0\)。于是你可以开 \(O(n)\) 个树状数组,每个询问变成了二维偏序,可以直接在树状数组上查询。

C

https://newoj.daimayuan.top/p/5627?tid=6a9ea29ce5a406ef8f304f07

容易想到先固定 \(k\) 只狼。

然后发现其实狼说啥没用。

于是我们可以把 \(R\) 转成 \(L\).

即令 \(b_i = k - b_i\)

形如:

0 0 0 0 0 wolf 1 1 1 1 1 wolf 2 2 2 2 wolf 3 3 3 3 ...

我们可以考虑 dp.

\(f_i\) 表示能否前 \(i\) 都成立。且 \(i\) 必定为村民。

\(i-1 \to i\) 当且仅当 \(b_i - b_{i - 1} = 0\)

\(i-2 \to i\) 当且仅当 \(b_i - b_{i - 2} = 1\).

这个是用 dp来刻画是比较清晰的。

\(dp_0 = 0\)

答案是 \(dp_{n + 1}\)\(b_{n + 1} = k\)

于是我们就有了 45 pts。

进一步的,我们要对于每个 \(k\) 都这么做。

容易发现只有当 \(b_i + b_j = C\) 某个定值时才有成立一条边。

也就是总边数是不多的。

我们枚举 \(k\) 的时候只会增删一些边求 dp 值。

这个直接上动态 dp 矩阵维护即可。

把 dp 刻画成矩阵的形式。

参考 \((+,\times)\) 矩阵,矩阵乘法需要满足两运算的条件:各自结合律 + 分配律。

这里令 \(e_{i,j}\) 表示有无这条边。

\(f_i = (f_{i-1}\cap e_{i-1,i}) \cup f_{i-2}\cap e_{i-2,i})\)

容易写成矩阵乘法形式,$(\cup,\cap) $ 矩乘。

线段树维护一下单点修改。

做完。

https://www.luogu.com.cn/problem/CF2152

树上带修问题可以考虑拍成序列做。

问虚树叶子个数。子树带修。

实在不行再考虑树链剖分。

咋拍成序列?\(dfn,euler\) 括号序。

容易发现对于括号序,一个黑点形成 \(()\) 那就是叶子。

线段树维护序列即可。

操作是子树颜色反转。

只需要对于括号序一段区间翻转即可。

模拟赛 4

100 + 100 + 40 + 10

A

B

平面上有 \(n\) 条线段,其中第 \(i\) 条线段连接了 \((0, a_i)\)\((T, b_i)\) 两个点。我们保证每条线段与至多 \(K\) 条其他的线段相交。

你要解决 \(q\) 个询问,每次询问给一个点 \((c, d)\)。考虑直线 \(x = c\),与这 \(n\) 条线段的交点为 \((c, y_1), (c, y_2), \dots, (c, y_n)\),求 \(\min |y_i - d|\)

做法

我们考虑把贡献写出来 \(ma + (1-m) b-d\).

\((a,b)\) 写在平面上。

注意到至多 \(k\) 个相交必然不可能存在长度为 \(k\) 的下降子序列。

由 dilworth 定理,自然可以划分成 \(k\) 个上升子序列。

直接对每个子序列做,二分即可。

C

若干区间,问排列数使得,每个前缀的并集都联通。

\(n \le 6000\)

做法

我们去想,如果我们对排列去做似乎是困难的。

一个排列前缀会带来一段覆盖区间。

不妨我们在区间上去考虑问题。

考虑区间 dp。

那我们可以去 dp 区间每次被扩充的过程。

形如 \([l_1,r_1],[l_2,r_2],[l_3,r_3] \dots [l_k,r_k]\)

\(c_i\) 表示区间内包含的区间个数。

     1 --- 2 ---- 3 ---- 4 ----- 5 ----- 6
     |     |
     |     |
    abc    cde

可以画出一个类似这样的树形结构。

这是一个树形拓扑序的结构,由经典结论可以推得。

对应排列数量为 \(n! /\prod (n - c_i)\).

考虑 \(f_{x,y}\) 会由什么转移过来。

就是去考虑原来的扩充区间是啥,我们选了啥区间才能变成 \([x,y]\) 这样。

钦定之后,我们就可以根据上面那个 \([l_1,r_1],[l_2,r_2],[l_3,r_3] \dots [l_k,r_k]\)

的式子去算了。

  • \(l=x,r=y \to [x',y'] \in[l,r]\)

  • \(l = x,r <y \to x'\in[l+1,r],y' = y\)

  • 同理

转移的优化也是自然的。

code

void solvemain()
{
	for (int i = 1;i < N;i ++ )
		inv[i] = ksm(i,mod -2 );
	// return;
	int n ;cin >> n;
	vector<int> L(n + 2,INF),R(n + 2,-INF);
	vector<vector<int> > ql(n + 2),qr(n + 2);
	rep(i,1,n) cin >> L[i]  >> R[i],cnt[L[i]][R[i]] ++ ,
	ql[L[i]].pb(R[i]),qr[R[i]].pb(L[i]) ;
	for (int len = 1;len <= n ;len ++ )
		for (int l = 1;l <= n;l ++ )
			cnt[l][l + len - 1] 
		+= cnt[l][l + len - 2] + cnt[l+ 1][l + len - 1] - cnt[l + 1][l + len - 2];
	auto add = [&](auto &a,auto b){a += b;if (a >= mod) a-= mod;};
	rep(i,1,n)
	{
		sort(all(ql[i]));
		sort(all(qr[i]),greater<int>());
	}
	for (int len = 1;len <= n;len ++ )
	{
		rep(L,1,n){
			const int R = L + len - 1;
			if (R > n) break;

			//f[L][R] -> f[x][y] 
			for (auto r : ql[L]){
				if (r > R) break;
				if (r == R)
				{//全覆盖
					for (int x = L;x <= R;x++)
					{
						const int lim = (x==L)?R-1:R;
						add(f[L][R],sumx[x][lim - x + 1]);
					}
					add(f[L][R],inv[n]);
				}
				else{
					const int y = R;
					add(f[L][R], (sumy[R][R-L] - sumy[R][R-r] + mod)%mod);
				}
			}
			for (auto l : qr[R])
			{

				if (l <= L) break;
				const int x =  L;
				add(f[L][R],(sumx[L][R-x] - sumx[L][l-x] + mod)%mod) ;
			}
			sumx[L][len] = (sumx[L][len - 1] + f[L][R] *1ll * inv[n-cnt[L][R]]%mod)%mod;
			sumy[R][len] = (sumy[R][len - 1] + f[L][R] * 1ll * inv[n-cnt[L][R]]%mod)%mod;
		}
	}
	int zuo = *min_element(all(L));
	int you = *max_element(all(R)); 	
	int jc = 1;
	rep(i,1,n) jc=  jc * 1ll * i %mod;
	cout<< f[zuo][you] * 1ll * jc%mod << endl;
}

总结:本题关键在于注意到用每次扩充区间来存储选择了哪些区间的状态,通过每次的区间扩容

我们可以方便的进行状态地转移

我们先钦定选择哪些作为扩充区间

我们了解到,每次扩容一个区间

都会增加一些可以任意选择的区间

刻画拓扑结构,是树上拓扑序模型

再用 dp 实现这个钦定的过程即可

DS 整理

操作可逆

例如,加法逆运算减法,异或,

https://www.luogu.com.cn/problem/P15534 ,逆运算是自己

可以考虑做前缀和差分

无修静态区间查

ST 表。不相交 ST(建线段树的结构,然后每个中点两边维护前后缀,通过某个中点分割区间,可以 \(O(1)\) 查询.

BIT 二分

int pos = 0;
// 从最高位开始向下枚举。1 << 19 约为 524288,可以根据具体 N 的大小调整
// 确保 (1 << i) 的最大值能够覆盖整个数组范围 N
for (int i = 19; i >= 0; --i) {
    int next_pos = pos + (1 << i);
    // 如果下一步没有越界,且当前积累的和还不够凑满 k
    if (next_pos <= n && tree[next_pos] < k) {
        pos = next_pos;
        k -= tree[pos]; // 巧妙之处:减去当前块的贡献,继续用剩余的 k 往后凑
    }
}

// 循环结束时的 pos 是满足 prefix_sum(pos) < 原k 的最大位置
// 因此 pos + 1 就是满足 prefix_sum(pos + 1) >= 原k 的第一个位置
return pos + 1;

区间信息不好合并?

考虑莫队。加入删除容易。

https://www.luogu.com.cn/problem/P15534

好题。

我们注意到这个信息可逆。

于是考虑差分。

路径相当于走上走下。

四次dfs 离线做询问即可。

int n , q,a[N],b[N];

vector<int> v[N];
int dfn[N],tot;
int fat[N];
int chk(int x,int y){return (dfn[x] < dfn[y]) ? x : y;}
int jp[N][21];
void dfs0(int x,int fa)
{
	fat[x] =fa;
	dfn[x]=++tot; jp[dfn[x]][0] = fa;
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs0(y,x);
	}		
}

// f_shang
int f[N],nf[N];
vector<int > rec[N];
int now[N];
void dfs1(int x,int fa)
{
	if (x == 1){for (int x = 1;x <= n;x ++ ) f[x] = x,nf[x] = x;}
	swap(f[a[x]],f[b[x]]);swap(nf[f[a[x]]],nf[f[b[x]]]);
	
	for (auto id : rec[x]) now[id] = f[now[id]];
	for (auto y : v[x]) if (y != fa) dfs1(y,x);
	swap(f[a[x]],f[b[x]]);swap(nf[f[a[x]]],nf[f[b[x]]]);

}
void dfs2(int x,int fa)
{
	if (x == 1){for (int x = 1;x <= n;x ++ ) f[x] = x,nf[x] = x;}
	swap(nf[a[x]],nf[b[x]]);swap(f[nf[a[x]]],f[nf[b[x]]]);
	for (auto &id : rec[x]) now[id] = f[now[id]];
	for (auto y : v[x]) if (y != fa) dfs2(y,x);
	swap(nf[a[x]],nf[b[x]]);swap(f[nf[a[x]]],f[nf[b[x]]]);
}

int lca(int x,int y)
{
	if (x == y) return x;
	x = dfn[x],y = dfn[y];if (x > y) swap(x,y);x ++ ;
	int k = __lg(y - x + 1);return chk(jp[x][k],jp[y-(1<<k)+1][k]);
}

void solvemain()
{
	cin >> n >> q;
	rep(i,1,n) cin >> a[i];rep(i,1,n)cin >>b[i];
	rep(i,1,n - 1)
	{
		int x,y;cin >> x >> y;v[x].pb(y);v[y].pb(x);
	}
	dfs0(1,0);
	for (int j = 1;j <= 20;j ++ )
		for (int i = 1;i + (1<<j) - 1 <= n;i ++ )
			jp[i][j] = chk(jp[i][j - 1],jp[i + (1<<j-1)][j - 1]); 		
	vct<array<int,2>> Q(q + 2);
	rep(i,1,q) cin >> Q[i][0] >> Q[i][1] >> now[i];


	rep(i,1, n) rec[i].clear();
	for (int i = 1,lc;i <= q;i ++ ) rec[Q[i][0]].pb(i);
	dfs1(1,0);
	// return;
	rep(i,1, n) rec[i].clear();
	for (int i = 1,lc;i <= q;i ++ )lc=lca(Q[i][0],Q[i][1]),rec[lc].pb(i);
	dfs2(1,0);
	
	rep(i,1,n) rec[i].clear();
	for (int i = 1,lc;i <= q;i ++ ) lc = lca(Q[i][0],Q[i][1]),rec[fat[lc]].pb(i);
	dfs1(1,0);
	rep(i,1,n) rec[i].clear();
	for (int i = 1;i <= q;i ++ )rec[Q[i][1]].pb(i);
	dfs2(1,0);
	rep(i,1,q) cout << now[i] << '\n';
}

区间限制下01串计数

区间至少/至多一个 1

dp 设计方法:\(f_i\) 表示只考虑 \([1,i]\).

再额外处理一下跨过 \(i\) 的即可。

注意到如果不钦定 \(i\) 选,我们一些跨过 \(i\) 的区间有没有被选依赖于后面的 \(j>i\).

不满足无后效性。

因此,我们直接钦定 \(i\) 选即可。

第二种方法:

\(dp_{i,j}\) 表示考虑 \([i,n]\) 上一个我们要考虑的信息在 \(j\)

可能是上一个同色的位置/选的位置...

冒泡操作小记

冒泡操作形态是可以直接刻画的。

前缀 \([1,l]\)\(c\) 轮冒泡后数字构成是区间 \([1,l + c]\) 中前 \(l\) 小的数字。

证明是显然的,一个数最多向前走 \(c\) 步。而小的数一定会向前走。

https://vjudge.net/contest/849236#problem/B

石头剪刀布 k 次冒泡。

如果是简单的排序是好做的。

所以我们考虑手玩一下这个操作。

一个较为显然的性质,我们从前往后考虑。

若前面有可以 beat 他的,那么他一定会向前移动。

因此容易发现可以划分连续段,每段内是极长的存在两个不同的。

段与段之间也是无法影响。

模拟赛 5

100 + 16 + 8 + 40 废

A

https://newoj.daimayuan.top/p/5631?tid=6aa7c1e6cc9d2bef7bc75748

字符串选区间翻转使得可以开心消消乐。

贪心地想,我们可以先删除已匹配的对。

剩下一坨。

注意到消消乐首尾也是可以匹配的。

我们去研究一下翻转 \([l,r]\) 后匹配的情况,

容易得到对于一个相邻不同的串,能搞完一定形如 \(U \ V \ V \ U^{rev}\)

B

https://newoj.daimayuan.top/p/5632?tid=6aa7c1e6cc9d2bef7bc75748

有向树,排列边,每次染色,按照边指向顺序染。

问能染 \(n-1\) 个点的排列数。

做法

一定有一个没染,考虑枚举作为 \(root\)

然后发现什么呢?

\(root\) 邻边一定是下指。

仔细研究一下。

每条边一定染底下的点。

对于下指边,肯定啥时候选都可以对吧。

我们记 \(p_x\) 表示 \(x\to fa\) 这条边。

对于向上指向的边,\(p_{fa}\) 要早于 \(p_x\)

形成拓扑序结构。

得到一片有向的森林。

根据树上 topo 序经典结论。

\(res = \prod_i 1/siz_i \times (n - 1) !\)

暴力计算 \(O(N^2)\)

考虑换根。

我们可以很容易求出 \(d(u,v)\) 表示 \(u\)\(v\) 这个方向的 \(subtree(v)\),一直往大的走,能走到多少点。

容斥一下可以求出。

后面就简单了。

换根往下走,只有 \((u,v)\) 反向,\(w \leftarrow w\times D(u,v) \times D(v,u)^{-1}\)

https://vjudge.net/contest/849236#problem/A

构造 \(2n\) 数组,使得存在一边大小是 \([2,n]\) 的划分使得两边和一致。

容易发现是比较紧的。纯看集合大小之和高达 \(2 + 3 + \dots + n = (n + 1) \times n / 2\)

而我们只有 \(2n\)。意味着我们肯定是很多重复的。

怎么样能节约呢?

不妨令 \(sum/2 = T\).

我们考虑第 \(i\) 个集合 \([1,i - 1]\) 构造,再加上 \(a_i = T - i(i-1)/2\).

目前已经使用 \(2n-3\) ,第 \(i\) 个集合只有 \(i\) 个,我们考虑加上一个固定常数 \(A\).

现在 \(sum/2 = A +T\)

只要再补齐常数即可。

https://vjudge.net/contest/849236#problem/C

小贪心

操作是删前删后,若前后相同可同时删(视为只删了1个。

问删除序列字典序最小。

考虑左右端点分别所处的连续段与其下一个连续段的字典序大小关系即可。

贪心地删。

CF2233F Shortest GCD Paths

https://www.luogu.com.cn/problem/CF2233F

有趣的题。

\(1e9\) 个点完全图边权为 \(\frac{max(u,v)}{\gcd(u,v)}\)

\(a\to b\) 最短路径.

不一定要从图论考虑,也可以想成是对 \(a\) 进行操作,操作成 \(b\).

怎么操作呢?

当题目中出现了一个特殊的式子,我们应该考虑如何刻画他。

容易发现 \(x\) 操作一次相当于丢掉一个因子 \(p\)\(\times q\)

代价为 \(\max(p,q)\)

接下来想,对于 \(a\to b\),我们会丢掉 \(g\) 以下的因子吗?

显然不会啊,反而会增加 \(max\)

于是我们只对于 \((a/g,b/g)\) 考虑。

有个问题是会不会超过范围 \(\le n\)

其实不会。

假设 \(a > b\).

我们经过一系列丢 \(x\)\(\times y\) 的操作得到 \(b\).

可以先丢大的再丢小的,容易发现一定超不过 \(a\)

接下来相当于把 \(a,b\) 分解成 \(x_1,x_2 \dots x_k,y_1,y_2 \dots y_k\)

\(\sum_i max(x_i,y_i)\) 最小。

注意到因子个数不多,考虑暴力转移?

算一下转移数

\[\sum _{x|b} \sum _{y|a} d(xy) \]

由于互质

\[\sum _{x|(ab)} d(x) \]

这个就是 \(d \times I\) 也是积性函数。

所以可以算每个 \(p^e\) 的贡献。

打个表就可以发现 \(\le 1.6e8\)

https://www.luogu.com.cn/problem/CF2025F

有趣的题。

\(q\) 个操作,初始全 \(0\),每次给出 \((x,y)\) 可以选择一个 \(\pm 1\)

注意到对于一个数一定是 \(+1 -1 +1 -1+1-1\)

对于任意操作,这种操作总体是最小的。

变成每条边选一个端点异或 \(1\)

\(1\) 个数最少多少。

我们可以先随便定一个端点,然后变成可以操作任意次对一条边端点异或 \(1\)

也就是 \(1\) 可以任意移动。

那就简单了。

对每个联通块

我们找一颗搜索树。

都移动到根的位置就好了。

做不明白的构造https://www.luogu.com.cn/problem/AT_agc030_c

对于一个 \(n \times n\) 的格子,我们用 \((r,\ c)\) 表示第 \(r+1\) 行第 \(c+1\) 列的格子。对于这个格子,用 \(K\) 种颜色的“好”涂色方式定义如下:

  • 每个格子都被 \(K\) 种颜色中的某一种涂色。
  • \(K\) 种颜色中的每一种颜色都至少出现在某个格子上。
  • \(K\) 种颜色分别编号为 \(1, 2, ..., K\)。对于任意颜色 \(i, j\)\(1 \leq i \leq K, 1 \leq j \leq K\)),所有颜色为 \(i\) 的格子,其相邻的颜色为 \(j\) 的格子的数量,都必须相等(不论选择哪个颜色为 \(i\) 的格子)。这里,格子 \((r, c)\) 的相邻格子指 \(((r-1)\bmod n, c),\ ((r+1)\bmod n, c),\ (r, (c-1)\bmod n),\ (r, (c+1)\bmod n)\)(如果这四个位置中有重复的格子,则该格子的颜色要按重复次数计数)。

给定 \(K\),请你自由选择 \(1 \leq n \leq 500\),构造一个 \(n \times n\) 的格子的 \(K\) 色“好”涂色方式。在本题的限制下,总是存在这样的构造。

做法

首先容易注意到 \(k \le 500\) 直接第 \(i\) 行填 \(i\).

考虑数据范围要 \(k \le 1000\)

也就是说应该是一行可能要贡献出两种颜色。

容易再想到一个构造第 \(i\)\(2i,2i-1\) 交替。

但是这个做法不好扩展啊。因为把一行换成一个数也不是对。

但是你灵机一动,把数斜过来填,就对了。

呃呃。

类似题:https://www.luogu.com.cn/problem/P12578

都是在矩形中构造可以考虑斜过来。

https://newoj.daimayuan.top/p/5636?tid=6aaa91b3cc9d2bef7bcdb8c5

好题

字符串 \(S\) 每次可以选一个 border.

然后令 \(T \leftarrow T +(S-border)\)

问恰好 \(k\) 次本质不同的 \(T\).

做法

首先要注意到一个结论。

我们把所有可能加的串拉出来。

注意到可能产生歧义的是互相有前缀关系的。

通过打表观察或者手玩,可以发现只有 \(T\) 的最小周期以及它的倍数会产生歧义。

kmp 算一下即可。

于是我们现在有 \(Z\) 个杂鱼字符串,\(H\) 个互相影响的。

这个 \(H\) 个形如:

X XX XXX XXXX XXXXX XXXXXX ...

问本质不同数量。

一个很经典的想法就是直接对最终串考虑。

要求这个串要能刚好用 \(k\) 次做出来。

转化一下就是 \(min \le k \le max\)

其中 \(min\) 类似于 \(\sum (len +k -1)/k + ...\)

\(max\) 就是一个个选。

在问题存在多重限制时,很简单的想法是我们通过容斥减掉一个限制。

于是我们可以求 \(min \le k - max < k\).

\(min\le k\) 那我们选择串的决策就变少了。

每次要么选 \(h\) 个要么选 \([0,h-1]\) 后接上一个杂鱼。

式子列出来是可以矩阵快速幂优化的,后面那一部分也是简单的。

while (q--){
    int m , k ;cin >>m >> k ;
    int B = ne[m];
    int h = m / (m - B);
    int z = cnt[m] + 1 - h;
    if (k <= 2)
    {
        if (k ==1) cout << 1 << endl;else
        cout << h + z << endl;
        continue;
    }
    if (h == 1)
    {
        cout << ksm(h + z,k - 1) << endl;
        continue;
    }
    k -- ;
    mat a;a.c[1][0] = 1;a.c[0][2] = a.c[0][1] = (h - 1)*1ll * z % mod;
    a.c[1][1] = a.c[1][2] = z + 1;
    a.c[2][2] = 1;
    a = jzksm(a,k - 1);
    int s = 1 * 1ll * a.c[0][2];
    add(s,(h+z)%mod *1ll *  a.c[1][2] % mod);
    add(s,(h + z + 1) % mod);
    if (z == 0) 
        s = (s - k + mod) % mod;
    else
        s = (s - ((ksm(z + 1,k) - 1+mod)%mod*1ll*ksm(z,mod - 2) % mod) +mod )%mod;

    cout <<s<< endl;
}

https://www.luogu.com.cn/problem/CF2233E2

简单记一下 CF补题。

把一个排列的二进制都排出来一个表格。

然后打乱每一位。问可能对应多少个排列?

做法

注意到给我们的这个排列其实没有意义。

因为不管怎么换,对于交换位数操作是无影响的。

\([1,n]\) 排列考虑

考虑有多少种排列位的方法使得还是 \([1,n]\)

手玩一下可以发现,当且仅当两个位 \(popcount\) 相同可以交换。

于是问题转化为能否变为 \([1,n]\).

注意到 \([1,n]\) 位数 \(popcount\) 递增,于是我们排个序即可。

void solvemain()
{
	int n ;cin >> n;
	int k =ceil( log(n + 1) / log (2) );
	struct node{
		string s;int cnt;
	};
	vct<node> v(k);
	for (int i = 0;i < k;i ++ )
	{
		string s;cin >> s;
		v[i].s = s;
		v[i].cnt = 0 ;
		for (auto t : s) if (t=='1') v[i].cnt++;
	}
	sort(all(v),[&](node a,node b){return a.cnt >b.cnt;});
	set<int> st;
	for (int i = 1;i <= n;i ++ )
	{
		int x = 0 ;
		for (int j = 0 ; j < k;j ++ ) x += (1<<j) * (v[j].s[i - 1]=='1');
		st.insert(x);
	}
	if (sz(st) == n && *st.begin()==1 && *st.rbegin()==n) {
		ll res = 1;
		vct<ll> jc(k + 2,1);
		rep(i,1,k) jc[i] = jc[i - 1] * 1ll * i ;
		for (int i = 0 ;i < k ;)
		{
			int j = i;
			while (j < k && v[j].cnt == v[i].cnt) j ++ ;
			res = res *jc[(j - i )] ;
			i = j ;
 		}
 		cout << res << endl;return;
	}
	cout << 0 << endl;
}
posted @ 2026-09-18 08:24  codwarm  阅读(7)  评论(0)    收藏  举报