2023 年牛客多校第二场题解

A Link with Checksum

题意:定义如下的类 CRC 的校验和算法。

string Link_CRC(string D)
{
    P = 0x04C11DB7;
    int n = D.length();
    D += "00000000 00000000 00000000 00000000";
    for (int i = 1; i <= n; i++)
    {
        if (D 的最高位为 1)
        {
            去掉 D 的最高位;
            D 的最高 32 位和 P 异或;
        }
        else
	        去掉 D 的最高位;
    }
    return D;
}

现给定一个长度为 n 1 n_1 n1 比特的 01 串 A A A 和长度为 n 2 n_2 n2 比特的 01 串 B B B,要求找到一个长度为 32 32 32 bit 的校验和 c h e c k s u m {\rm checksum} checksum,使得 L i n k _ C R C ( A + c h e c k s u m + B ) = c h e c k s u m {\rm Link\_CRC}(A+{\rm checksum}+B)={\rm checksum} Link_CRC(A+checksum+B)=checksum 1 ≤ n 1 , n 2 ≤ 1 0 5 1 \le n_1, n_2 \le 10^5 1n1,n2105

解法:不难注意到,无论最高位是什么,都会被去掉。且 P P P 只有 26 26 26 位,显然不符合 CRC 运算中模数比余数长一位的规则。考虑令 P = 0 x 104 C 11 D B 7 P={\rm 0x104C11DB7} P=0x104C11DB7(增加第 33 33 33 位为 1 1 1),同时修改规则使得这个类 CRC 变成一个真正的 CRC 运算:

F 2 32 \mathbb{F}_{2^{32}} F232

string CRC(string D)
{
    P = 0x104C11DB7;
    int n = D.length();
    丢弃 D 的先导 0;
    while (位数 >= 33)
    {
        if (D 的最高位为 1)
            D 最高 33 位异或 P;
        去掉 D 的最高位;
    }
    取 D 最后 32 位作为 D;
    return D;
}

既然是传统 CRC,那么必然满足 CRC 的各种性质

  1. 每个二进制位彼此独立。即满足形如 C R C ( x ⊕ y ) = C R C ( x ) ⊕ C R C ( y ) {\rm CRC}(x \oplus y)={\rm CRC}(x) \oplus {\rm CRC}(y) CRC(xy)=CRC(x)CRC(y)

  2. C R C ( 2 x ) = { 2 C R C ( x ) , x  最高位为  0 2 C R C ( x ) ⊕ P , x  最高位为  1 {\rm CRC}(2x)= \begin{cases} 2{\rm CRC}(x),x {\text\ 最高位为\ }0\\ 2{\rm CRC}(x)\oplus P,x {\text\ 最高位为\ }1 \end{cases} CRC(2x)={2CRC(x),x 最高位为 02CRC(x)P,x 最高位为 1

利用上述两个性质,可以提前将每一位上有 1 1 1 的余数计算出来。由于校验和的长度固定,因而给出的 A A A 串和 B B B 串的 C R C {\rm CRC} CRC 余数 r r r 可以提前计算。现在问题转化为求一长度为 32 32 32 的 01 串 x x x,需要满足 r ⊕ C R C ( x × 2 8 n 2 ) = x r \oplus {\rm CRC}(x\times 2^{8n_2})=x rCRC(x×28n2)=x

由于串长仅 32 32 32,可以考虑折半搜索——枚举和存储高 16 16 16 位的情况,然后再枚举低 16 16 16 位的结果与之匹配。

#include 
using namespace std;
const int N = 2000000;
const long long P = 0x104C11DB7ll;
long long bit[N + 5];
int a[N + 5], b[N + 5];
int main()
{
	bit[0] = 0x04C11DB7ll; // 注意:和朴素CRC不同,该方法的CRC的0x1(边界条件)的余数不是1,而是0x04C11DB7。
	for (int i = 1; i <= N; i++)
	{
		bit[i] = bit[i - 1] << 1;
		if (bit[i] >> 32) // 取最高位
			bit[i] ^= P;
	}
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	for (int i = 1; i <= m; i++)
		scanf("%d", &b[i]);
	int pos = 0;
	long long AB = 0;
	for (int i = m; i >= 1; i--, pos += 8)
		for (int j = 7; j >= 0; j--)
			if (b[i] >> j & 1)
				AB ^= bit[pos + j];
	int startpos = pos;
	pos += 32;
	for (int i = n; i >= 1; i--, pos += 8)
		for (int j = 7; j >= 0; j--)
			if (a[i] >> j & 1)
				AB ^= bit[pos + j];
    // 折半搜索
	map<long long, long long> highpos; // 记录高 16 位的答案
	for (int i = 0; i < 1 << 16; i++)
	{
		long long cur = 0;
		for (int j = 0; j < 16; j++)
			if (i >> j & 1)
				cur ^= bit[startpos + 16 + j];
		highpos[cur ^ ((long long)i << 16) ^ AB] = i;
	}
    // 枚举低位
	for (int i = 0; i < 1 << 16; i++)
	{
		long long cur = 0;
		for (int j = 0; j < 16; j++)
			if (i >> j & 1)
				cur ^= bit[startpos + j];
		if (highpos.count(cur ^ i))
		{
			printf("%lld", (highpos[cur ^ i] << 16) | i);
			return 0;
		}
	}
	printf("-1");
	return 0;
}

这样做的时间复杂度为 O ( 16 n + 2 16 ) \mathcal O(16n+2^{16}) O(16n+216)

当然,本题的 CRC 位数不够多,当位数达到 64 64 64 或者正常 CRC 的 512 512 512 2048 2048 2048 位时,显然不支持折半搜索。这时不难注意到我们可以根据各位余数贡献独立的条件构建异或方程。我们可以统计余数的某一个二进制位可能会受哪些原输入串的二进制位影响(即原串上该位置有 1 1 1 会给余数的某一位上带来一次 1 1 1 的异或),从而列出方程。这样就可以使用线性基(01 高斯消元)以更快速的解决本题。

B Link with Railway Company

题意:给定一棵 n n n 个点的树状铁路网,树上每条边表示一段铁路,边权值表示该段铁路的维护费用。现在开行 m m m 条线路,经过树上的一条链,开行第 i i i 条线路获利 x i − y i x_i-y_i xiyi。现在降本增效,选择其中一部分线路开行,选择开行的线路经过的每条铁路都需要投入维护费,如果该段铁路没有线路经过则可以废弃,问最大获利是多少。 1 ≤ n , m ≤ 1 0 4 1 \le n,m \le 10^4 1n,m104

解法:我们首先不考虑时间复杂度,我们首先考虑一个最基本的一个图模型,也就是就是当前的这条线路以及对应的这个线路上所有的铁路段,我们把它构成一个依赖关系,也就是说,我们必须这个线路上所有的铁路都要维护,这个时候才能够选择这条线路并进行运营获利。那这个是我们其实构成了一个很经典的一个问题,就是最大权闭合子图——即选择一个点必须选择它所有可到达的后继节点。这个问题可以通过如下的网络流建图得到:

  1. 从源点出发向所有的正权点(有出边的点,本题中为线路代表的点)连流量为点权值的边;
  2. 从所有没有出边的点(即负权点,本题中为铁路对应的边)向汇点连接边流量为点权的负数(即点权的绝对值)的边。
  3. 如果存在依赖关系,则将依赖点(线路)向被依赖点(铁路)连接边流量为无穷大的边。

这样最大权值为所有正权点的和减去图的最小割(最大流)。

这样做的原理是,考虑其中某一条线路和它依赖的铁路集合。如果当前线路盈利能力较强,可以覆盖铁路线路的支出,那么当前最大流等于支出的代价,最后的贡献为线路收入减去支出的代价;如果线路盈利能力不足以覆盖铁路维护成本,则当前该线路的最大流的贡献等于盈利,最后对总收入的贡献抵消为零,即停止运营本条线路。

但是本题中依赖关系的边过多,直接根据依赖关系建立边数量可以达到 O ( n m ) O(nm) O(nm),无法接受。但是不难注意到本题铁路线路构成一棵树,用树链剖分可以将线路对应的铁路构成 O ( log ⁡ n ) O(\log n) O(logn) 段树链,因而使用线段树优化建图可以实现 O ( m log ⁡ 2 n ) O(m\log^2 n) O(mlog2n) 的边条数,从而可以通过本题。

#include 
#define int long long
const int INF = 1e15;
#define MAXN 100010
using namespace std;
struct node
{
	int v, id;
	node *nxt;
} edge[MAXN];
node *head[MAXN], *ncnt = edge;
int cnt;
vector<int> a[MAXN], w[MAXN], rev[MAXN];
int n, m, tot, tot1, s, t;
void link_edge(int u, int v, int id)
{
	ncnt++;
	ncnt->nxt = head[u];
	ncnt->v = v;
	ncnt->id = id;
	head[u] = ncnt;
}
int A[MAXN];
int dfn[MAXN], top[MAXN], fa[MAXN], siz[MAXN], dep[MAXN], son[MAXN], fai[MAXN], rnk[MAXN], num[MAXN];
void find_son(int x, int f = 0)
{
	siz[x] = 1;
	dep[x] = dep[f] + 1;
	for (node *i = head[x]; i != NULL; i = i->nxt)
	{
		int v = i->v;
		if (v == f)
			continue;
		fai[v] = i->id;
		fa[v] = x;
		find_son(v, x);
		siz[x] += siz[v];
	}
	for (node *i = head[x]; i != NULL; i = i->nxt)
	{
		int v = i->v;
		if (v == f)
			continue;
		if (siz[v] > siz[son[x]] || son[x] == 0)
			son[x] = v;
	}
}
void prep(int x, int tp, int f = 0)
{
	top[x] = tp;
	dfn[x] = ++cnt;
	rnk[cnt] = x;
	if (son[x] == 0)
		return;
	prep(son[x], tp, x);
	for (node *i = head[x]; i != NULL; i = i->nxt)
	{
		int v = i->v;
		if (v == f || v == son[x])
			continue;
		prep(v, v, x);
	}
}
void add_edge(int u, int v, int val)
{
	a[u].push_back(v);
	a[v].push_back(u);
	w[u].push_back(val);
	w[v].push_back(0);
	rev[u].push_back(a[v].size() - 1);
	rev[v].push_back(a[u].size() - 1);
}
int pl[MAXN], pr[MAXN];
void build(int id, int l = 1, int r = n)
{
	if (l == r)
	{
		add_edge(id, t, A[fai[rnk[l]]]);
		num[id] = fai[rnk[l]];
		return;
	}
	int mid = (l + r) >> 1;
	pl[id] = ++tot;
	pr[id] = ++tot;
	build(pl[id], l, mid);
	build(pr[id], mid + 1, r);
	add_edge(id, pl[id], INF);
	add_edge(id, pr[id], INF);
}
void add_edge_insegtree(int tar, int l1, int r1, int id = 3, int l = 1, int r = n)
{
	if (l >= l1 && r <= r1)
	{
		add_edge(tar, id, INF);
		return;
	}
	int mid = (l + r) >> 1;
	if (l1 <= mid)
		add_edge_insegtree(tar, l1, r1, pl[id], l, mid);
	if (r1 > mid)
		add_edge_insegtree(tar, l1, r1, pr[id], mid + 1, r);
}
void add_edge_intree(int id, int u, int v)
{
	while (top[u] != top[v])
	{
		if (dfn[top[u]] < dfn[top[v]])
			swap(u, v);
		add_edge_insegtree(id, dfn[top[u]], dfn[u]);
		u = fa[top[u]];
	}
	if (dep[u] < dep[v])
		swap(u, v);
	if (u != v)
		add_edge_insegtree(id, dfn[son[v]], dfn[u]);
}
int d[MAXN], g[MAXN];
int dfs(int x, int flw)
{
	if (x == t)
		return flw;
	int flw1 = flw;
	for (int i = 0; i < (int)(a[x].size()); i++)
	{
		int u = a[x][i];
		if (w[x][i] != 0 && d[u] == d[x] - 1)
		{
			int f = dfs(u, min(flw, w[x][i]));
			w[x][i] -= f;
			w[u][rev[x][i]] += f;
			flw -= f;
			if (flw == 0)
				break;
			if (d[s] >= tot)
				return flw1 - flw;
		}
	}
	if (flw != flw1)
		return flw1 - flw;
	if (g[d[x]] == 1)
	{
		d[s] = tot;
		return 0;
	}
	g[d[x]]--;
	int minh = tot;
	for (int i = 0; i < (int)(a[x].size()); i++)
		if (w[x][i] != 0)
			minh = min(minh, d[a[x][i]]);
	d[x] = minh + 1;
	g[d[x]]++;
	return 0;
}
int maxflow()
{
	g[0] = tot;
	int ans = 0;
	while (d[s] < tot)
		ans += dfs(s, INF);
	return ans;
}
bool vis[MAXN];
void gov(int x)
{
	vis[x] = 1;
	for (int i = 0; i < (int)(a[x].size()); i++)
		if (w[x][i] != 0 && vis[a[x][i]] == 0)
			gov(a[x][i]);
}
vector<int> dogman, dogedge;
signed main()
{
	scanf("%lld%lld", &n, &m);
	int u, v;
	for (int i = 1; i < n; i++)
	{
		scanf("%lld%lld%lld", &u, &v, &A[i]);
		link_edge(u, v, i);
		link_edge(v, u, i);
	}
	find_son(1);
	prep(1, 1);
	s = 1;
	t = 2;
	tot = 2;
	build(++tot);
	tot1 = tot;
	int ans = 0;
	for (int i = 1; i <= m; i++)
	{
		int x, y;
		scanf("%lld%lld%lld%lld", &u, &v, &x, &y);
		if (x <= y)
			continue;
		ans += x - y;
		add_edge(s, i + tot, x - y);
		add_edge_intree(i + tot, u, v);
	}
	tot += m;
	printf("%lld\n", ans - maxflow());
	return 0;
}

C Graph

题意:给定一个 G ( n , m ) G(n,m) G(n,m),现在给每个点黑色或白色,使得同色点形成的每个生成子图都有欧拉路。 1 ≤ n ≤ 100 1 \le n\le 100 1n100 1 ≤ m ≤ 1 0 3 1 \le m \le 10^3 1m103

解法:有欧拉路的条件可以加强为生成子图的每个点的度为偶数。

考虑用方程来表达约束条件。对于第 i i i 个点,假设 x i x_i xi 为它最终分配的颜色,那么为满足它的生成子图度为偶数,考虑以下两种情况:

  1. x i x_i xi 最终为白点,即 x i = 0 x_i=0 xi=0。则此时周围为黑点的个数应和 i i i 的度的奇偶性相同,即方程中系数非零的项中黑点( x j = 1 x_j=1 xj=1)的项个数奇偶性和 i i i 的度的奇偶性相同。
  2. x i x_i xi 最终为黑点,即 x i = 1 x_i=1 xi=1。则此时周围为黑点的个数应为偶数个,即方程中系数非零的项中黑点( x j = 1 x_j=1 xj=1)的项个数为偶数。

整合起来即可以写成 d e g i x i + ∑ ( i , k ) ∈ E x k = deg ⁡ i \displaystyle {\rm deg}_i x_i+\sum_{(i,k) \in E}x_k={\deg}_i degixi+(i,k)Exk=degi。接下来使用 bitset进行高斯消元即可。具体代码可以参看https://ac.nowcoder.com/acm/contest/view-submission?submissionId=62950557。复杂度 O ( n 3 w ) \mathcal O\left(\dfrac{n^3}{w}\right) O(wn3)

D The Game of Eating

题意: n n n 个人 m m m 道菜,现在需要点 k k k 道菜,每个人轮流点一个。每个人对每个菜有一个评分 { a } ( i , j ) = ( 1 , 1 ) n , m \{a\}_{(i,j)=(1,1)}^{n,m} {a}(i,j)=(1,1)n,m,且每个人对不同菜的评分互不相同。每个人都按照这 k k k 个菜的自我评判标准最优(即如果点菜集合为 { p 1 , p 2 , ⋯   . p k } \{p_1,p_2,\cdots.p_k\} {p1,p2,.pk},则第 i i i 个人最大化 ∑ j = 1 k a i , p j \displaystyle \sum_{j=1}^k a_{i,p_j} j=1kai,pj)点菜。问最终会点哪些菜。多组询问, 1 ≤ T ≤ 1 0 3 1 \le T \le 10^3 1T103 ∑ n ≤ 2 × 1 0 3 \sum n \le 2\times 10^3 n2×103 ∑ m ≤ 2 × 1 0 3 \sum m \le 2\times 10^3 m2×103

解法:如果正着贪心考虑点菜,那么会出现一个问题——我当前点的菜确实是我现在最喜欢的,但是如果我点我次喜欢的菜,然后后面有人也跟我同好喜欢我最喜欢的菜,那么我不如点我次喜欢的,这样我就能获得我最喜欢和次喜欢的菜了。

不妨倒着考虑这个问题——当现在还有一个菜的份额的时候,这个人一定会点自己最喜欢的菜——后续没人再帮他点菜了。再回退一步,当现在还剩两个菜的份额的时候,既然已知最后一个人会点他最喜欢的,我就点除了这个菜之外最喜欢的菜,依次类推。

因而最终就是倒着考虑最喜欢的菜,加入菜单即可。

#include 
using namespace std;
#define int long long
void solve()
{
    int n, m, k;
    cin >> n >> m >> k;
    vector<vector<int>> a(n, vector<int>(m));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cin >> a[i][j];
    vector<int> us(m, false);
    vector<int> ans;
    for (int i = k - 1; i >= 0; i--)
    {
        int now = i % n;
        int t = -1;
        for (int j = 0; j < m; j++)
            if (!us[j])
            {
                if (t == -1 || a[now][j] > a[now][t])
                    t = j;
            }
        us[t] = true;
        ans.push_back(t + 1);
    }
    sort(ans.begin(), ans.end());
    for (auto it : ans)
        cout << it << " ";
    cout << "\n";
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

E Square

题意:给定 x x x,求一个数 y y y 使得 0 ≤ y ≤ 1 0 9 0 \le y \le 10^9 0y109 y 2 y^2 y2 的最高若干位恰好为 x x x 0 ≤ x ≤ 1 0 9 0 \le x \le 10^9 0x109,多组询问, 1 ≤ T ≤ 1 0 5 1 \le T\le 10^5 1T105

解法:首先特判 x = 0 x=0 x=0 y = 0 y=0 y=0

可以考虑枚举 y 2 y^2 y2 的位数是多少。枚举 k k k,寻找满足 [ x × 1 0 k , ( x + 1 ) × 1 0 k ) \left[\sqrt{x\times 10^k},\sqrt{(x+1)\times 10^k}\right) [x×10k ,(x+1)×10k ) y y y 即可。注意此处 x × 1 0 k \sqrt{x\times 10^k} x×10k 可能很大,因而最好使用二分答案来确定该区间上下界。单组时间复杂度 O ( 10 log ⁡ 1 0 18 ) \mathcal O(10\log 10^{18}) O(10log1018)

#include 
using namespace std;
long long cal(long long x) // 二分开根号
{
	long long l = 1, r = 1'000'000'000, ans = -1;
	while (l <= r)
	{
		long long mid = (l + r) >> 1;
		if (mid * mid >= x)
		{
			ans = mid;
			r = mid - 1;
		}
		else
			l = mid + 1;
	}
	return ans;
}
int main()
{
	int t;
	long long x, y;
	scanf("%d", &t);
	while (t--)
	{
		scanf("%lld", &x);
		if (!x)
		{
			printf("0\n");
			continue;
		}
		long long ans = -1;
        // 枚举位数
		for (long long th = 1, i = 1; i <= 10; i++, th *= 10)
		{
			long long curl = x * th;
			long long low = cal(curl);
			if (low * low / th == x)
			{
				ans = low;
				break;
			}
		}
		printf("%lld\n", ans);
	}
	return 0;
}

F Link with Chess Game

题意:长度为 n n n 的链上给定三个颜色不同的棋子的位置,这些棋子可以重叠。两个人一轮移动其中一枚棋子,不能出现之前已经出现的状态,不能操作者输,问结果。多组测试数据, 1 ≤ T ≤ 1 0 5 1 \le T \le 10^5 1T105 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105

解法:首先我们需要考虑一个基本博弈模型:二分图博弈。二分图博弈是指双方在二分图上某个点放置一枚棋子,双方轮流移动棋子,且不能将棋子移动到已经走过的点。结论是,如果起始点不在最大匹配(即去掉该点,则匹配数减 1 1 1)上,则先手必败。因为可以考虑沿着一条最大匹配链走,如果起始点在最大匹配上,那么先手可以一直轩匹配点走,而后手则可能找不到这样的点。

考虑现在这个模型。我们可以将当前的状态放置在一个棱长为 n n n 的正方体上,每移动某一种颜色的棋子相当于是在某个维度上移动一步。首先不难发现,棱长为 n n n 的正方体上所有整点和与坐标轴平行的边构成一个二分图(可以用黑白染色的结论证明)。那么考虑起始的点是不是在最大匹配上。

对于 n n n 为偶数的情况,显然每个点都在完美匹配上——考虑固定 z z z 坐标,仅在和 x O y xOy xOy 平行的平面上,该正方体 n n n z z z 轴剖面正方形都是可以完美匹配的。因而这种情况先手必胜。

对于 n n n 为奇数的情况,则要考察起始点的情况。

  1. 如果起始点是黑点(三个维度加起来的和为奇数),则一定存在一种匹配方式使得它不在完美匹配点上,因而这种情况先手必败。
  2. 如果当前点为白点,即三个维度加起来的和为偶数,则必然先手必胜。因为黑点的个数比白点多,因而所有的白点都可以被匹配,也就是它一定在完美匹配上。

G Link with Centrally Symmetric Strings

题意:定义镜像回文——回文中心是空或 o,s,z,x,然后 o,s,z,x自身跟自身匹配,(b,q),(d,p),(n,u)匹配。给定串 S S S,问是否可以将 S S S 分解为若干镜像回文串的拼接。多组询问, ∑ ∣ S ∣ ≤ 1 0 6 \sum |S| \le 10^6 S106

解法:考虑一个最简单的 DP 状态: f i f_i fi 表示前缀 i i i 是否可以被拆分成若干个镜像回文串的拼接。

那么其实只需要去从前缀 i i i 的最长回文后缀 j j j 转移(即,最小的 j j j 满足 [ j + 1 , i ] [j+1,i] [j+1,i] 为镜像回文串)即可。这是因为如果考虑前缀 i i i 更短的回文后缀 k k k,那么它一定可以被从前缀 j j j 处再加两段回文串拼接而成—— [ j + 1 , j + 1 + ( i − k − 1 ) − 1 ] [j+1,j+1+(i-k-1)-1] [j+1,j+1+(ik1)1] [ j + 1 + ( i − k − 1 ) , k ] [j+1+(i-k-1),k] [j+1+(ik1),k] [ k + 1 , i ] [k+1,i] [k+1,i]。也就是说 f j = f k f_j=f_k fj=fk。所以每个点只需要从其最长回文后缀处转移即可。CF1827C 和本题在这个处理上非常类似。

基于这个性质,我们只需要找到每个点对应的最长回文后缀是多少,可以使用 Manacher 算法实现——使用并查集,在 Manacher 算法执行匹配过程的时候,维护每个字符最远匹配的点。

但是我们需要对传统 Manacher 算法进行改造才能适用于本题镜像回文的情况。首先是对字符串字符匹配的判定——zxso是自身和自身匹配,而剩下几个配对的字符之间需要互相匹配。同时 b,p,d,q,n,u不能作为回文串中心,即以他们为中心的回文半径为 0 0 0

#include 
using namespace std;
const int maxn=1e6+10;
const int inf =2e9;
char g[500];
void init(){
    g['o']='o';
    g['s']='s';
    g['x']='x';
    g['z']='z';
    g['b']='q';
    g['q']='b';
    g['p']='d';
    g['d']='p';
    g['u']='n';
    g['n']='u';
    g['#']='#';
    g['@']='0';
}
struct ST{
	char s[maxn * 2], str[maxn * 2];
	int Len[maxn * 2], len;
	void getstr() {//重定义字符串
		int k = 0;
		len = strlen(s);
		str[k++] = '@';//开头加个特殊字符防止越界
		for (int i = 0; i < len; i++) {
			str[k++] = '#';
			str[k++] = s[i];
		}
		str[k++] = '#';
		len = k;
		str[k] = 0;//字符串尾设置为0,防止越界
	}
    bool same(char c,char c2){
        // cout<
        return g[(int)c]==c2;
    }
	int manacher() {
		int mx = 0, id=0;//mx为最右边,id为中心点
		int maxx = 0;
		for (int i = 1; i < len; i++) {
			if (mx > i) Len[i] = min(mx - i, Len[2 * id - i]);//判断当前点超没超过mx
			else Len[i] = 1;//超过了就让他等于1,之后再进行查找
            if(!same(str[i],str[i]))Len[i]=0;
                // cout<<"I="<
			while (i-Len[i]>=0&&same(str[i + Len[i]], str[i - Len[i]])) {
                Len[i]++;//判断当前点是不是最长回文子串,不断的向右扩展
                // cout<<"i="<
            
            }
			if (Len[i] + i > mx) {//更新mx
				mx = Len[i] + i;
				id = i;//更新中间点
				maxx = max(maxx, Len[i]);//最长回文字串长度
			}
		}
		return (maxx - 1);
	}
	void writ(){
		printf("%s\n",str);
    	for(int i=0;i<len;i++){
    	    cout<<Len[i];
    	}
		cout<<"\n";
	}
};
ST s1;
struct Sol{
    int n;
    int ans[maxn];
    int f[maxn];
    void set_n(int _n){
        n=_n;
        for(int i=1;i<=n+3;i++)f[i]=i;
        for(int i=1;i<=n+3;i++)ans[i]=inf;
    }
    int getf(int x){
        if(x==f[x])return f[x];
        else return f[x]=getf(f[x]);
    }
    void merge(int x,int y){//x--->y
        x=getf(x),y=getf(y);
        if(x==y)return;
        f[x]=y;
    }
    void g(int l,int r,int i){
        for(l=getf(l);l<=r;l=getf(l)){
            // cout<
            ans[l]=i;
            merge(l,l+1);
        }
    }
};
Sol sol;
void solve()
{
    scanf("%s",s1.s);
    int n=strlen(s1.s);
    s1.getstr();
    s1.manacher();
    // s1.writ();
    vector<int> dp(n+2,0);
    sol.set_n(n);
    for(int i=2;i<=2*n;i++){
        int len=s1.Len[i];
        if(len/2>0&&s1.same(s1.str[i],s1.str[i])){
            sol.g(i/2-len/2+1,i/2,i);
        }
    }
    dp[n+1]=1;
    for(int i=n;i>=1;i--){
        int z=sol.ans[i];
        if(z!=inf){
            dp[i]|=dp[z-i+1];
            // cout<
        }
    }
    // cout<
    if(dp[1])printf("Yes\n");
    else printf("No\n");
    // for(int i=1;i<=n;i++)cout<
}
signed main()
{
    init();
    int T;scanf("%d",&T);while(T--)solve();
    return 0;
}

H 0 and 1 in BIT

题意:对一个 01 串定义两种操作: A A A 表示将这个串 01 反转; B B B 表示将这个 01 串视为一个二进制数(左侧为最低位),然后执行加一操作。 q q q 次询问,给出一个仅由 A B AB AB 构成的长度为 n n n 的操作序列 S S S,每次给出一个 01 串,问执行完 S [ l ; r ] S[l;r] S[l;r] 后该 01 串变成什么。 1 ≤ n , q ≤ 2 × 1 0 5 1 \le n,q \le 2\times 10^5 1n,q2×105,每次询问的 01 串长度不超过 50 50 50,强制在线。

解法:考虑补码的性质——对于一个二进制数 x x x − x -x x 等效于每个 01 位翻转,然后执行加一操作。因而如果将原串视为一个二进制数 x x x,则 A A A 操作可以视为 x ← − x − 1 x \leftarrow -x-1 xx1,而 B B B 操作则为 x ← x + 1 x \leftarrow x+1 xx+1

根据这个转化,我们发现,可以考虑每个字符对于最终答案的影响,例如 B A B A BABA BABA 中第一个 B B B 对答案的影响是 + 1 +1 +1(因为后面有两个 A A A),第一个 A A A 对答案的影响是 + 1 +1 +1(因为后面有一个 A A A),第二个 B B B 对答案的影响是 − 1 -1 1(因为后面有一个 A A A),第二个 A A A 对答案的影响是 − 1 -1 1(因为后面什么都没了)。且顺便也能发现对于一段确定的区间 ( l , r ) (l,r) (l,r) 无论输入的 x x x 是什么,变化都是 x x x 先乘以 1 1 1 − 1 -1 1(取决于这段里 A A A 的奇偶性),在模 2 64 2^{64} 264 意义下加或减同一个常数,我们就是要求出每个区间的这个常数,记所求的东西为 f ( l , r ) f(l,r) f(l,r)

转化后的问题可以用矩阵加线段树或加倍增或前缀求矩阵逆来维护,但本题也可以使用前缀和。

c n t ( l , r ) cnt(l,r) cnt(l,r) 表示 ( l , r ) (l,r) (l,r) A A A 的数量,可以前缀和预处理。同时我们也可以前缀和预处理出前缀的答案 f ( 1 , 1 ) , f ( 1 , 2 ) , ⋯   , f ( 1 , n − 1 ) , f ( 1 , n ) f(1,1),f(1,2),\cdots,f(1,n-1),f(1,n) f(1,1),f(1,2),,f(1,n1),f(1,n)

对于一次询问 ( l , r ) (l,r) (l,r):若 c n t ( l , r ) cnt(l,r) cnt(l,r) 是偶数,则有 f ( 1 , l − 1 ) + f ( l , r ) = f ( 1 , r ) f(1,l-1)+f(l,r)=f(1,r) f(1,l1)+f(l,r)=f(1,r);若 c n t ( l , r ) cnt(l,r) cnt(l,r) 是奇数,则有 − f ( 1 , l − 1 ) + f ( l , r ) = f ( 1 , r ) -f(1,l-1)+f(l,r)=f(1,r) f(1,l1)+f(l,r)=f(1,r),因此都可以解出 f ( l , r ) f(l,r) f(l,r)。另外注意当 c n t ( l , r ) cnt(l,r) cnt(l,r) 是奇数时,要把输入的 x x x 先乘以 − 1 -1 1

增补说明:不妨这样考虑这个问题。 f ( 1 , r ) f(1,r) f(1,r) 积累了从 1 1 1 变化到 r r r 的变化,记作 a 2 x + b 2 a_2x+b_2 a2x+b2 f ( 1 , l − 1 ) f(1,l-1) f(1,l1) 同理,记作 a 1 x + b 1 a_1x+b_1 a1x+b1。现在假设从 l l l 开始操作,那么现在起始的数值为 x ’ = a 1 x + b 1 x’=a_1x+b_1 x=a1x+b1,可以解得如果从 1 1 1 开始初始的 x = x ′ − b 1 a 1 x=\dfrac{x'-b_1}{a_1} x=a1xb1,这时再从 1 1 1 变化到 r r r,则答案为 a 1 x ′ − b 1 a 2 + b 2 \dfrac{a_1}{x'-b_1}a_2+b_2 xb1a1a2+b2。这里的 a ∈ { − 1 , 1 } a \in \{-1,1\} a{1,1}

本题和 AGC044C 类似,但是本题要求强制在线,有兴趣可以了解一下。

#include 
using namespace std;
const int N = 1000000;
int cnt[N + 5], f[N + 5];
char s[N + 5], x[N + 5];
long long trans(char *x)
{
    int k = strlen(x + 1);
    long long ans = 0;
    for (int i = 1; i <= k; i++)
        ans = ans * 2 + x[i] - '0';
    return ans;
}
int main()
{
    long long n, q;
    scanf("%lld%lld", &n, &q);
    scanf("%s", s + 1);
    for (int i = 1; i <= n; i++)
    {
        cnt[i] = cnt[i - 1] + (s[i] == 'A');
        if (s[i] == 'A')
            f[i] = -f[i - 1] - 1;
        else
            f[i] = f[i - 1] + 1;
    }
    long long l, r, lastans = 0;
    while (q--)
    {
        scanf("%lld%lld%s", &l, &r, x + 1);
        l = (l ^ lastans) % n + 1;
        r = (r ^ lastans) % n + 1;
        if (l > r)
            swap(l, r);
        assert(l >= 1 && l <= n);
        assert(r >= l && r <= n);
        long long cur = trans(x);
        int dif, type = (cnt[r] - cnt[l - 1]) % 2;
        if (type)
        {
            cur = -cur;
            dif = f[r] + f[l - 1];
        }
        else
            dif = f[r] - f[l - 1];
        cur += dif;
        lastans = 0;
        int k = strlen(x + 1);
        for (int i = k - 1; i >= 0; i--)
            if (cur >> i & 1)
            {
                printf("1");
                lastans |= 1ll << i;
            }
            else
                printf("0");
        puts("");
    }
    return 0;
}

I Link with Gomoku

题意:给定 n × m n\times m n×m 的棋盘上面玩五子棋,黑手先,轮流下棋使得最终局面为平局,没有禁手。多组测试数据,满足 ∑ n × m ≤ 1 0 6 \sum n\times m \le 10^6 n×m106

解法:由于需要是一个可以一步一步下出来的局面,因而黑白棋个数需要考虑——当 n × m n \times m n×m 为偶数时要相同,为奇数时黑比白多一个。

尽可能考虑在两行内实现黑白棋子个数相同,如果列数为偶数时能够一行内就相同。不难得到这样的双行构造:

偶数时:
xoxoxoxoxoxoxo
oxoxoxoxoxoxox
奇数时:
xoxoxoxoxoxox
oxoxoxoxoxoxo

但是如果简单这样拼接会导致斜行出现五连(下方大写的 X):

xoxoxoxoXoxoxo
oxoxoxoXoxoxox
xoxoxoXoxoxoxo
oxoxoXoxoxoxox
xoxoXoxoxoxoxo
oxoxoxoxoxoxox

为避免这种情况,可以考虑让 3 − 4 3-4 34 两行交换:

xoxoxoxoxoxoxo
oxoxoxoxoxoxox
oxoxoxoxoxoxox
xoxoxoxoxoxoxo
xoxoxoxoxoxoxo
oxoxoxoxoxoxox
oxoxoxoxoxoxox
xoxoxoxoxoxoxo

这样就可以避免斜行的问题了。

最后注意一个 corner case 就是这样安排之后可能 ox更多,这个时候需要交换二者以实现黑手先(黑比白多)。

#include 
using namespace std;
const int N = 1000;
int s[N + 5][N + 5];
int main()
{
	int t, n, m;
	scanf("%d", &t);
	while (t--)
	{
		scanf("%d%d", &n, &m);
		int cntx = 0, cnto = 0;
		for (int i = 0; i < n; i++)
		{
			int cur = (i % 4 <= 1) ? 0 : 1; // 模4为23的时候要交换两行
			int op = (i & 1) ^ cur;
			for (int j = 1; j <= m; j++)
			{
				if (op == 0)
					s[i + 1][j] = 1, cntx++;
				else
					s[i + 1][j] = 0, cnto++;
				op ^= 1;
			}
		}
		int rev = 0;
		if (cntx < cnto) // 交换ox
			rev = 1;
		for (int i = 1; i <= n; i++, puts(""))
			for (int j = 1; j <= m; j++)
				if (s[i][j] ^ rev)
					printf("x");
				else
					printf("o");
	}
	return 0;
}

K Box

题意:给定长度为 n n n 的序列 { a } i = 1 n \{a\}_{i=1}^n {a}i=1n a i a_i ai 表示第 i i i 个盒子被盖子保护起来之后可以获得的价值。现在有一些地方有盖子,每个盖子至多只能往左或往右移动一格,问能被盖子保护起来的价值最大值。 1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1n106 1 ≤ a i ≤ 1 0 9 1 \le a_i \le 10^9 1ai109

解法:显然,不同盖子之间相对位置关系不会发生变化,而每个盖子只有三种状态——往左移动一格(用 0 0 0 表示),不动( 1 1 1),右移一格( 2 2 2)。设 f i , j f_{i,j} fi,j 表示前 i i i 个盖子在 j j j 状态时所能保护的最大值,记第 i i i 个盖子在 p i p_i pi 处,则不难得到下面的 dp 方程:
f i , j = max ⁡ k = 0 , 1 , 2 ( f i − 1 , k + a p i + j − 1 ) , s . t . p i − 1 + k − 1 ≤ p i + j − 1 f_{i,j}=\max_{k=0,1,2}(f_{i-1,k}+a_{p_i+j-1}), {\rm s.t.} p_{i-1}+k-1 \le p_i+j-1 fi,j=k=0,1,2max(fi1,k+api+j1),s.t.pi1+k1pi+j1
总时间复杂度 O ( n ) \mathcal O(n) O(n)

#include 
using namespace std;
const int N = 1000000;
long long f[N + 5][3], a[N + 5];
int pos[N + 5], cnt, b[N + 5];
int main()
{
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%lld", &a[i]);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &b[i]);
		if (b[i])
			pos[++cnt] = i;
	}
	if (!cnt)
	{
		printf("0");
		return 0;
	}
	memset(f, 0xcf, sizeof(f)); // 初始化为全-inf
	f[1][1] = a[pos[1]];
	if (pos[1] > 1)
		f[1][0] = a[pos[1] - 1];
	if (pos[1] < n)
		f[1][2] = a[pos[1] + 1];
	for (int i = 2; i <= cnt; i++)
		for (int j = 0; j <= 2; j++)
			for (int k = 0; k <= 2; k++)
				if (pos[i - 1] + k - 1 < pos[i] + j - 1 && pos[i] + j - 1 <= n)
                    // 不越界,盖子之间相对位置关系保持
					f[i][j] = max(f[i][j], f[i - 1][k] + a[pos[i] + j - 1]);
	printf("%lld", max({f[cnt][0], f[cnt][1], f[cnt][2]}));
	return 0;
}

你可能感兴趣的:(算法)