[Week 20]每日一题(C++,图论,数学,搜索)

T1 [Daimayuan] Collision(C++,多源最短路)

题目描述

\(siyisss\) 的王国是由 \(n\) 个村镇和 \(n−1\) 条双向道路构成的,村镇从 \(1\)\(n\) 依次编号,每条双向道路连接两个不同的村镇,使得从任意一个村镇出发都可以到达任意一个村镇。接下来请你回答 \(q\) 个问题,每次给出两个整数 \(c\), \(d\),表示现在分别有一个人在村镇 \(c\),一个人在村镇 \(d\),现在在 \(c\) 的人要用最短的时间到达村镇\(d\),在村镇 \(d\) 的人要以最短的时间到达村镇$ c$,假设两人同时出发,两人的速度也是一样的,每条双向道路的长度也是一样的,请问两人相遇的时候是在某一个村镇,还是在某条双向道路上?

输入描述

第一行输入两个整数 \(n\), \(q\) 代表村镇的数量和询问的数量

接下来 \(n−1\) 行,每行两个整数用来描述一条双向道路

最后 \(q\),每行两个整数代表 \(c\), \(d\)

输出描述

对于每个询问,如果他们在某个村镇相遇,请示出Town,否则输出Road

样例输入1

5 2
1 2
2 3
3 4
4 5
1 3
1 5

样例输出1

Town
Town

样例输入2

9 9
2 3
5 6
4 8
8 9
4 5
3 4
1 9
3 7
7 9
2 5
2 6
4 6
2 4
5 8
7 8
3 6
5 6

样例输处2

Town
Road
Town
Town
Town
Town
Road
Road
Road

数据范围

\(2≤n≤100000\)

\(1≤q≤100000\)

对于每一个询问 \(1≤c_i<d_i≤n\)

解题思路

题意是给出一张无向连通图,然后询问\(q\)次,每次要求判断\(c\),\(d\)两人相遇的位置。

根据数据范围,我们需要将每次询问的时间复杂度降至\(O(1)\)或者\(O(log\ q)\)

理解题意之后,第一个问题就是如何判断两人相遇的位置:

(1)如果最短路的长度为奇数,那么两人在Road相遇;

(2)如果最短路的长度为偶数,那么两人在Town相遇。

关于最短路的算法,我们最容易想到的是Floyd。但是\(n\in [2,100000]\),直接\(T\)飞掉。

回顾题意:\(n\)个节点、\(n-1\)条边、全连通。

这不就是一棵树嘛。

那么我们要知道树的特点:任意两个节点之间只有一条通路。

我们只需要找出最近公共父节点(LCA),就可以得出两个节点之间的通路长度。

采用倍增寻访算法,时间复杂度为\(O(qlog\ q)\),可以接受。

AC代码如下:

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <string.h>
using namespace std;
const int max_n = 1e5;
const int max_expo = 32;

struct edge { int v, next; }edges[max_n * 2];
int tot = -1, heads[max_n + 1];
int dp[max_n + 1][max_expo];
int lg[max_n + 1];
int depth[max_n + 1];


void add_edge(int u, int v) {
	edges[++tot] = { v,heads[u] }; heads[u] = tot;
	edges[++tot] = { u,heads[v] }; heads[v] = tot;
}

void init(int s, int fa) {
	for (int i = 1; i < lg[depth[s]]; i++) {
		dp[s][i] = dp[dp[s][i - 1]][i - 1];
	}

	for (int i = heads[s]; i != -1; i = edges[i].next) {
		int v = edges[i].v;
		if (v != fa) {
			dp[v][0] = s;
			depth[v] = depth[s] + 1;
			init(v, s);
		}
	}
}

int lca(int x, int y) {
	if (depth[x] < depth[y]) swap(x, y);
	while (depth[x] != depth[y]) x = dp[x][lg[depth[x] - depth[y]] - 1];
	if (x == y) return x;
	for (int i = lg[depth[x]]; i >= 0; i--) {
		if (dp[x][i] != dp[y][i]) {
			x = dp[x][i];
			y = dp[y][i];
		}
	}
	return dp[x][0];
}

int main() {
	int n, q, u, v;
	//cin >> n >> q;
	scanf("%d%d", &n, &q);
	for (int i = 1; i <= max_n; i++) {
		lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
	}
	memset(heads + 1, -1, sizeof(int) * n);
	for (int i = 0; i < n - 1; i++) {
		//cin >> u >> v;
		scanf("%d%d", &u, &v);
		add_edge(u, v);
	}

	dp[1][0] = 1;
	depth[1] = 1;
	init(1, 1);

	for (int i = 0; i < q; i++) {
		//cin >> u >> v;
		scanf("%d%d", &u, &v);
		int ret = lca(u, v);
		//if ((depth[ret] * 2 - depth[u] - depth[v]) % 2 == 0) cout << "Town" << endl;
		//else cout << "Road" << endl;
		if ((depth[ret] * 2 - depth[u] - depth[v]) % 2 == 0) printf("Town\n");
		else printf("Road\n");
	}
	return 0;
}

T2 [Daimayuan] 农田划分(C++,数学,BFS)

题目描述

约翰是一个农场主,他的农场有n块田,编号从 \(1\)\(n\),这 \(n\)块田通过 \(m\)条双向道路相连(数据保证这\(n\)块田都是联通的),我们假设第\(i\)块田会产生 \(2^ikg\) 的收益,现在约翰想把农田的工作全部交给自己的两个孩子,划分方式必须满足以下规则:

1.每一块田都需要恰好被分给一个孩子.

2.分给两个孩子的农田必须是联通的.就是说对于任意一个孩子在划分给自己的任意一块田,都可以不经过另外一个孩子的田,到达自己的任意一块田.

3.划分给两个孩子的收益必须尽可能的相等,如果无法相等,年长的孩子会得到大的那一份.

对于第 \(i\)块田,如果你要把它分给年长的孩子,请输出A,否则输出B.

题目输入

第一行输入两个整数分别代表 \(n,m\) 接下来 \(m\)行,每个两个整数\(u,v\),代表这两块农田通过一条双向道路直接相连,数据保证没有重边和自环

题目输出

输出一个字符串,代表答案

样例输入1

3 2
1 3
3 2

样例输出1

ABA

样例输入2

6 6
3 5
2 6
1 3
3 6
5 1
4 6

样例输出2

BABABA

数据范围

\(2≤n≤3e5,1≤m≤3e5\)

解题思路

首先,我们要知道:

对于数列\([1,2,2^2,...,2^{n-1},2^n]\)\(2^n\)是大于之前所有的数加和的。

利用这个性质,因为我们要保证年长的孩子分得的土地更多,所以\(n\)号田地一定会被分给年长的孩子。

继续利用这个性质,因为我们又要保证划分尽可能的相等,所以\(n-1\)号田地一定会被划分给另外一个孩子。

这有什么用呢?

我们的具体算法是这样的:

删除\(n\)号田地及与其相连的所有通路,然后从\(n-1\)号田地开始\(BFS\),所有被搜索到的田地均分给另外一个孩子,剩下的田地归年长的孩子。

算法时间复杂度为\(O(n)\)

最后,AC代码如下:

#include <iostream>
#include <queue>
#include <string.h>
using namespace std;
const int max_n = 3e5;
const int max_m = 3e5;

struct edge { int v, next; }edges[max_m * 2];
int tot = -1, heads[max_n + 1];
queue<int>q;
bool book[max_n + 1];

void add_edge(int u, int v) {
	edges[++tot] = { v,heads[u] }; heads[u] = tot;
	edges[++tot] = { u,heads[v] }; heads[v] = tot;
}

void bfs(int s) {
	q.push(s);//初始化

	//bfs主体
	while (!q.empty()) {
		int head = q.front();
		q.pop();//取出首节点

		if (book[head]) continue;
		book[head] = true;//访问判断

		for (int i = heads[head]; i != -1; i = edges[i].next) {//进行尝试
			int v = edges[i].v;
			if (book[v]) continue;//重复访问
			q.push(v);
		}
	}
}

int main() {
	int n, m, u, v;
	cin >> n >> m;
	memset(heads + 1, -1, sizeof(int) * n);
	for (int i = 0; i < m; i++) {
		cin >> u >> v;
		if (u == n || v == n) continue;//逻辑删除
		add_edge(u, v);
	}
	bfs(n - 1);
	for (int i = 1; i <= n; i++) {
		if (book[i]) cout << 'B';
		else cout << 'A';
	}
	return 0;
}

T3 [Daimayuan] 三段式(C++,数组前缀和)

有一个长度为\(n\)的序列,现在我们想把它切割成三段(每一段都是连续的),使得每一段的元素总和都相同,请问有多少种不同的切割方法

输入描述

第一行给出一个数\(n\),(\(1≤n≤10^5\))

第二行给出序列\(a_1\)\(a_2\)\(a_3\),...,\(a_n\),(\(|a_i|≤10^5\))

输出描述

输出一个数表示有多少种不同的切割方法

样例输入

4
1 2 3 3

样例输出

1

样例解释

可以将它分成第一组\(1\)\(2\),第二组\(3\),第三组\(3\)

解题思路

根据题意,很容易得到下面的公式:

\[sum_1+sum_2+sum_3=sum\\ sum_1=sum_2=sum_3\\ \\ sum_1=sum_2=sum_3=\frac{1}{3}sum \]

要满足题意有两个前提条件(特殊判定):

(1)\(sum\ \%\ 3==0\)

(2)\(n\ge3\)

if (n < 3 || sum % 3 != 0) {
    cout << 0 << endl;
    return 0;
}

然后我们来寻找可能的分割方式。

如果有多于\(1\)种的分割方法,那么一定存在子段和为\(0\)

与其去找子段和为\(0\)的部分,不如转化思维,使用前缀和的概念(经常用于连续区间和问题)。

这样我们就只需要寻找前缀和同为\(\frac13sum\)的部分了。

然后具体问题具体分析,因为题目中只要求分割为三段,所以我们直接找出前后两段\(\frac13sum\)即可。

注意,至少要为中间的\(\frac13sum\)预留一个元素的位置。

long long ans = 0;
for (int i = 1; i <= n - 1; i++) {
	if (pre[i] == subsum) {
		ans += tail_counter[i + 2];
	}
}

其中tail_counter中的元素意义为:\(i\)位置及其之后有多少个后缀和为\(\frac13sum\)

最后,AC代码如下:

#include <iostream>
using namespace std;
const int max_n = 1e5;

long long arr[max_n + 1], pre[max_n + 1], tail[max_n + 2];
long long tail_counter[max_n + 1];

int main() {
	int n;
	long long sum = 0;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> arr[i];
		sum += arr[i];
	}

	if (n < 3 || sum % 3 != 0) {
		cout << 0 << endl;
		return 0;
	}
	long long subsum = sum / 3;
	for (int i = 1; i <= n; i++) {
		pre[i] = arr[i] + pre[i - 1];
	}
	for (int i = n; i >= 1; i--) {
		tail[i] = arr[i] + tail[i + 1];
		tail_counter[i] = tail_counter[i + 1];
		if (tail[i] == subsum) tail_counter[i]++;
	}
	long long ans = 0;
	for (int i = 1; i <= n - 1; i++) {
		if (pre[i] == subsum) {
			ans += tail_counter[i + 2];
		}
	}
	cout << ans << endl;
	return 0;
}

T4 [Daimayuan] 模拟输出受限制的双端队列(C++,模拟)

给你一个输出受限的双端队列,限制输出的双端队列即可以从一端插入元素,弹出元素,但是另一端只可以插入不可以删除元素。即每次你可以执行以下三种操作的其中一种:

  1. 在左边压入一个字符
  2. 在右边压入一个字符
  3. 弹出最左边的字符

image-20220516202010515

现在给你 \(n\) 个字符作为队列的输入,请问最多有多少可能的出队次序,并按字典序打印这些出队次序。

输入格式

第一行一个数 \(n\),表示输入的长度

第二行一个长度为 \(n\) 的字符串

输出格式

第一行一个整数 \(k\),表示可能的出队方案数

下面 \(k\) 行,按字典序输出每种出队方案

样例输入

3
123

样例输出

6
123
132
213
231
312
321

数据规模

对于全部数据保证 \(1≤n≤7\)

解题思路

虽然题目说是模拟,但是我们不尝试一下别的方法肯定不会甘心的,所以尝试理解序列满足的规则。

以样例输入为例子,我们留出三个位置\(□□□\)

考虑一种简单的情况,我们在所有元素入队之后再进行出队操作,那么一下逻辑成立:

首先对于\(a_1\),我们可以任意指定它的位置;

然后对于\(a_2\),因为\(a_1\)的位置已经确定了,\(a_2\)必须在\(a_1\)的前方或者\(a_1\)的后方,其位置选择受限;

同理可知\(a_3\)位置选择同样受限。

似乎并没有可以用来优化代码的规律存在,所以我们还是老老实实回去模拟。

模拟双端队列采用STL提供的deque容器,由于搜索的结果存在重复,采用set存储结果去重(同时set会自动将结果按字典序排序)。

#include <iostream>
#include <deque>
#include <set>
using namespace std;
const int max_n = 7;

string in_str;
int n;
set<string>ans;

采用dfs搜索可能的出队方案:

void dfs(int step, deque<char>d, string str) {//dfs状态
	//终止条件
	
	//dfs主体
	//返回后操作
}

接下来我们对dfs代码功能进行实现。

实现具体的代码之前我们需要知道每一步有几种可能的操作:

1)在首部插入一个元素;

d.push_front(in_str[step]);
dfs(step + 1, d, str);
d.pop_front();

2)在尾部插入一个元素;

d.push_back(in_str[step]);
dfs(step + 1, d, str);
d.pop_back();

3)出队一个元素;

while (!d.empty()) {
	str += d.front(); d.pop_front();
	//在首部插入一个元素
	d.push_front(in_str[step]);
	dfs(step + 1, d, str);
	d.pop_front();
	//在尾部插入一个元素
	d.push_back(in_str[step]);
	dfs(step + 1, d, str);
	d.pop_back();
}

以上步骤实现完后,终止条件显而易见:

if (step == n) {
	while (!str.empty()) {
		str += d.front(); d.pop_front();
		ans.insert(str);
	}
	return;
}

dfs完整代码如下:

void dfs(int step, deque<char>d, string str) {//dfs状态
	//终止条件
	if (step == n) {
		while (!str.empty()) {
			str += d.front(); d.pop_front();
			ans.insert(str);
		}
		return;
	}
	//dfs主体
	//在首部插入一个元素
	d.push_front(in_str[step]);
    dfs(step + 1, d, str);
    d.pop_front();//返回后操作
    //在尾部插入一个元素
    d.push_back(in_str[step]);
    dfs(step + 1, d, str);
    d.pop_back();//返回后操作
    //出队一个元素
    while (!d.empty()) {
        str += d.front(); d.pop_front();
        //在首部插入一个元素
        d.push_front(in_str[step]);
        dfs(step + 1, d, str);
        d.pop_front();//返回后操作
        //在尾部插入一个元素
        d.push_back(in_str[step]);
        dfs(step + 1, d, str);
        d.pop_back();//返回后操作
    }
}

最后,AC代码如下:

#include<iostream>
#include <set>
#include <deque>
using namespace std;
const int max_n = 7;

int n;
string in_str;
set<string>ans;

void dfs(int step, deque<char>d, string str) {//dfs状态
	//终止条件
	if (step == n) {
		while (!d.empty()) {
			str += d.front(); d.pop_front();
		}
		ans.insert(str);
		return;
	}
	//dfs主体
	//在首部插入一个元素
	d.push_front(in_str[step]);
	dfs(step + 1, d, str);
	d.pop_front();//返回后操作
	//在尾部插入一个元素
	d.push_back(in_str[step]);
	dfs(step + 1, d, str);
	d.pop_back();//返回后操作
	//出队一个元素
	while (!d.empty()) {
		str += d.front(); d.pop_front();
		//在首部插入一个元素
		d.push_front(in_str[step]);
		dfs(step + 1, d, str);
		d.pop_front();//返回后操作
		//在尾部插入一个元素
		d.push_back(in_str[step]);
		dfs(step + 1, d, str);
		d.pop_back();//返回后操作
	}
}

int main() {
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	deque<char>d;
	cin >> n;
	cin >> in_str;
	dfs(0, d, "");
	cout << ans.size() << endl;
	for (auto iter : ans) cout << iter << endl;
	return 0;
}

T5 [Daimayuan] 简单差分(C++,线段树)

给定一个长度为 \(n\) 数组 \(A\),执行以下操作 \(m\) 次:

选择一段区间 \([l,r]\),将区间中所有的数加上整数 \(x\)

操作完成后回答 \(k\) 个问题:

每个问题给定一段区间 \([l,r]\),输出区间中所有数的和。

输入格式

第一行三个正整数 \(n,m,k\)

接下来一行 \(n\) 个数,表示数组 \(A\)

接下来 \(m\) 行,每行输入三个整数 \(l,r,x\)

接下来 \(k\) 行,每行输入两个整数 \(l,r\)

输出格式

输出 \(k\) 行,每行一个数表示对应问题的和。

样例输入

10 1 1
1 2 3 4 5 6 7 8 9 10
5 8 1
8 9

样例输出

18

数据规模

对于全部数据,保证 \(1≤n≤2×10^5\)\(1≤m,k≤10^5\)\(|x|≤10^5\)

解题思路

线段树模板题,不做过多解释。

不了解线段树的可以看一下这个dalao的博客:线段树详解 - Xenny - 博客园

AC代码如下:

#include <iostream>
using namespace std;
const int max_n = 2e5;
const int max_m = 1e5;
const int max_k = 1e5;
const int max_x = 1e5;

long long tree[max_n * 4 + 1], arr[max_n + 1];
long long lazy_tags[max_n * 4 + 1];

void build_tree(int idx, int l, int r) {
	if (l == r) {
		tree[idx] = arr[l];
		return;
	}

	int m = l + r >> 1;
	build_tree(idx << 1, l, m);
	build_tree((idx << 1) + 1, m + 1, r);
	tree[idx] = tree[idx << 1] + tree[(idx << 1) + 1];
}

void push_down(int idx, int l, int r) {
	if (lazy_tags[idx]) {
		int m = l + r >> 1;
		lazy_tags[idx << 1] += lazy_tags[idx];
		tree[idx << 1] += lazy_tags[idx] * (long long)(m - l + 1);
		lazy_tags[(idx << 1) + 1] += lazy_tags[idx];
		tree[(idx << 1) + 1] += lazy_tags[idx] * (long long)(r - m);
		lazy_tags[idx] = 0;
	}
}

void update(int idx, int l, int r, int L, int R, long long val) {
	if (L <= l && r <= R) {
		tree[idx] += (long long)(r - l + 1) * val;
		lazy_tags[idx] += val;
		return;
	}
	push_down(idx, l, r);
	int m = l + r >> 1;
	if (L <= m) update(idx << 1, l, m, L, R, val);
	if (R >= m + 1) update((idx << 1) + 1, m + 1, r, L, R, val);
	tree[idx] = tree[idx << 1] + tree[(idx << 1) + 1];
}

long long query(int idx, int l, int r, int L, int R) {
	if (L <= l && r <= R) {
		return tree[idx];
	}
	push_down(idx, l, r);
	int m = l + r >> 1;
	long long sum = 0;
	if (L <= m) sum += query(idx << 1, l, m, L, R);
	if (R >= m + 1) sum += query((idx << 1) + 1, m + 1, r, L, R);
	return sum;
}

int main() {
	int n, m, k;
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++) cin >> arr[i];
	build_tree(1, 1, n);
	int l, r, x;
	for (int i = 0; i < m; i++) {
		cin >> l >> r >> x;
		update(1, 1, n, l, r, x);
	}
	for (int i = 0; i < k; i++) {
		cin >> l >> r;
		cout << query(1, 1, n, l, r) << endl;
	}
	return 0;
}

T6 [Daimayuan] 非递减的01序列(C++,前缀和)

题面

\(huaji\)有一个 01 序列,每次可以对其中的某一位取反(\(0\)\(1\)\(1\)\(0\)

求最少翻转其中的几位可以使得该序列变为非递减序列

输入格式

第一行输入一个整数 \(n\) (\(1≤n≤10^6\))

第二行输入一个长度为 \(n\) 的且仅包含 01 的字符串

输出格式

输出一个整数,为该序列变为非递减序列的最少操作次数

输入样例

6
010110

输出样例

2

解题思路

其实题意描述有一点问题,求的应该是将该序列变为单调不减的01序列(因为没有单调性其实也是非递减)。

我们的目标具体来说就是选定某个位置,之前的均为0,之后的均为1,同时保证与原序列相似性最高。

根据这个目标,我们只需要遍历每一个可能的位置,选出其中需要操作次数最少的就可以了。

问题在于如何快速获取操作次数。

因为只有两种元素:0和1。所以我们只需要知道其中一种的数量,剩下的都是另外一种。

那么采用前缀和的概念,累计指定位置前\(0\)的数量。

//首元素特殊处理
if (str[0] == '0') zero_counter[0] = 1;
else one_counter[0] = 1;

for (int i = 1; i < len; i++) {
	zero_counter[i] = zero_counter[i - 1];
	if (str[i] == '0') zero_counter[i]++;
}

然后遍历每一个位置即可:

int ans = zero_counter[len - 1];//目标序列没有0
for (int i = 0; i < len; i++) {
	ans = min(ans, (i + 1 - zero_counter[i]) + (zero_counter[len - 1] - zero_counter[i]));
}

最后,AC代码如下:

#include <iostream>
using namespace std;
const int max_len = 1e6;

int zero_counter[max_len];

int main() {
	int n;
	cin >> n;
	string str;
	cin >> str;
	int len = str.size();
	if (str[0] == '0') zero_counter[0] = 1;
	for (int i = 1; i < len; i++) {
		zero_counter[i] = zero_counter[i - 1];
		if (str[i] == '0') zero_counter[i]++;
	}
	int ans = zero_counter[len - 1];//目标序列没有0
	for (int i = 0; i < len; i++) {
		ans = min(ans, (i + 1 - zero_counter[i]) + (zero_counter[len - 1] - zero_counter[i]));
	}
	cout << ans << endl;
	return 0;
}

T7 [Daimayuan] 数学(C++,最大公约数)

给定整数 \(n\),胖教授想将\(1∼n\)\(n\)个数字分成两组,每一组至少有一个数,并且使得两组数字的和的最大公约数最大,请输出最大的最大公约数。

输入格式

一行一个整数\(n\)

输出格式

输出一行一个整数表示答案。

样例输入

6

样例输出

7

数据规模

对于\(20\%\)的数据,保证\(n≤100\)

对于\(100\%\)的数据,保证\(n≤10^9\)

解题思路

我们从最大公约数的概念入手:

\(x\)\(y\)的最大公约数是\(z\),那么\(x\)\(y\)都是\(z\)的倍数。

那么我们所要寻找的最大公约数一定能够整除\(sum=(1+n)*n/2\)

要找到最大的最大公约数,我们就只需要找到一个最小的数,使其能整除\(sum\)即可。

AC代码如下:

#include<iostream>
using namespace std;

int main() {
	long long n;
	cin >> n;
	long long sum = (n + 1) * n / 2;
	for (long long i = 2; i * i <= sum; i++)
		if (sum % i == 0) {
			printf("%lld", sum / i);
			break;
		}
	return 0;
}

T8 [Daimayuan] pSort(C++,强连通分量)

题目描述

有一个由 \(n\) 个元素组成的序列 \(a_1,a_2,…,a_n\);最初,序列中的每个元素满足 \(a_i=i\)

对于每次操作,你可以交换序列中第 \(i\) 个元素和第 \(j\) 个元素当且仅当满足 \(|i−j|=d_i\)

题目给出序列 \(b_1,b_2,…,b_n\)\(d_1,d_2,…,d_n\),询问是否可以通过若干次交换,使得序列 \(a\) 和序列 \(b\) 完全相同。

输入格式

\(1\) 行一个正整数 \(n\),含义如上。

\(2\)\(n\) 个正整数表示 \(b_1,b_2,…,b_n\)

\(3\)\(n\) 个正整数表示 \(d_1,d_2,…,d_n\)

输出格式

若能,输出 YES;否则输出 NO

输入 #1

7
4 2 5 1 3 7 6
4 6 6 1 6 6 1

输出 #1

YES

输入 #2

7
4 3 5 1 2 7 6
4 6 6 1 6 6 1

输出 #2

NO

数据范围

\(1≤n,d_i≤100\)。保证序列 \(b\) 中元素不重复。

补充说明

原题:CF28B pSort | 洛谷

解题思路

对于题意的理解如下:

if (abs(i - j) == d[i] || abs(i - j) == d[j]) {
	/* i,j是可交换的 */
}

那么我们可以把\(a\)序列抽象为一张无向图,可交换关系为无向边。

则强连通分量之内的节点可以随意交换。

也就是说,如果需要交换所有节点都在同一个强连通分量之中,就输出YES

反之,如果需要交换的任意一对节点不在一个强连通分量中,就输出NO

接下来实现代码:

我们采用染色法标记不同的强连通分量,用广度优先搜索对整张图进行染色,时间复杂度为\(O(n^2)\)

void bfs(int bg) {
	color++;
	q.push(bg);

	int head, next, i;
	while (!q.empty()) {
		head = q.front();
		q.pop();

		if (book[head]) continue;
		book[head] = true;
		colors[head] = color;

		for (i = 1; i <= n; i++) {
			if (i == head) continue;
			next = abs(head - i);
			if (next == ds[head] || next == ds[i]) q.push(i);
		}
	}
}

我们需要遍历每一个节点进行尝试,所以算法总时间复杂度为\(O(n^3)\),可以接受。

for (i = 1; i <= n; i++) {
	if (!colors[i]) bfs(i);
}

最后,AC代码如下:

#include <iostream>
#include <queue>
#include <cmath>
using namespace std;
const int max_n = 100;

int bs[max_n], ds[max_n];
int colors[max_n], color = 0;
queue<int>q;
int book[max_n];
int n;

void bfs(int bg) {
	color++;
	q.push(bg);

	int head, next, i;
	while (!q.empty()) {
		head = q.front();
		q.pop();

		if (book[head]) continue;
		book[head] = true;
		colors[head] = color;

		for (i = 1; i <= n; i++) {
			if (i == head) continue;
			next = abs(head - i);
			if (next == ds[head] || next == ds[i]) q.push(i);
		}
	}
}

int main() {
	cin >> n;
	int i;
	for (i = 1; i <= n; i++) cin >> bs[i];
	for (i = 1; i <= n; i++) cin >> ds[i];
	for (i = 1; i <= n; i++) {
		if (!colors[i]) bfs(i);
	}

	for (i = 1; i <= n; i++) {
		if (colors[bs[i]] != colors[i]) {
			cout << "NO" << endl;
			return 0;
		}
	}
	cout << "YES" << endl;
	return 0;
}

T9 [Daimayuan] Owwwwwwwwwww...f(C++,强连通分量)

\(A\)地盘上的所有人被从 \(1\)\(n\) 编号,每个人都有自己传话的对象,第 \(i\) 个人对第 \(a_i\)个人传话。 有一天,小\(A\)在宫殿的顶部大声喊着\(Owf\),于是一个有趣的游戏在小\(A\)的地盘上开始了。

规则如下:

该游戏有许多轮,每个人都会开始一轮游戏。如果编号为 \(x\) 的人想要开始一轮游戏,他会对第 \(a_x\)个人说"\(Oww...wwf\)"(有 \(t\)\(w\))。如果 \(t>1\),第 \(a_x\)个人就会对第 \(a_{ax}\)个人说"\(Oww...wwf\)"(有 \(t−1\)\(w\))。直到有人听到"\(Owf\)"(\(t=1\)),这个人就是这一轮的\(Joon\)。不存在同时进行两轮游戏的情况。 为了使游戏更有意思,小\(A\)有一个邪恶的计划。他想找到最小的 \(t\)\(t≥1\))使得对于每个人 \(x\) 当第 \(x\) 个人开始的一局游戏使 \(y\) 成为了 \(Joon\) ,也使得由 \(y\) 开始的一局游戏 \(x\) 成为 \(Joon\) 。请为小\(A\)找这个最小的 \(t\)。 注意:可能有的人传话对象是自己。

输入格式:

第一行输入一个 \(n\) (\(1≤n≤150\)),表示小A地盘上的人数。

第二行输入\(a_1\)\(a_2\)\(a_3\),...\(a_n\),第 \(i\) 个数表示第 \(i\) 个人传话的对象 \(a_i\)

输出格式:

输出最小的 \(t\),如果没有请输出 \(−1\)

样例输入:

4
2 3 1 4

样例输出:

3

解题思路:

把题中序列抽象为一张有向图,有\(n\)个节点、\(n\)条有向边\(<i,a_i>\)

如果能够达成题中所述的双向传话,两个人必须在一个环中。

如果环的长度为偶数,那么这个环的传话次数为其长度一半;

如果环的长度为奇数,那么这个环的传话次数为其长度。

我们需要做的就是统计每一个环的传话次数,然后计算最小公倍数即可。

很简单对吧qwq?

那么现在来实现代码。

首先是搜索环的长度:

void dfs(int bg) {
	int next = as[bg], sum = 1;
	book[bg] = true;
	while (next != bg) {
		if (book[next]) {
			fail = true;
			return;
		}
		sum++;
		book[next] = true;
		next = as[next];
	}
	if (sum % 2 == 0) ans.push_back(sum / 2);
	else ans.push_back(sum);
}

然后计算所有ans的最小公倍数:

long long ret = 1;
for (auto iter : ans) {
	ret = lcm((long long)(iter), ret);
}
cout << ret << endl;

后排提醒:/* 十年OI一场空,不开long long见祖宗 */

最后,AC代码如下:

#include <iostream>
#include <vector>
using namespace std;
const int max_n = 150;

int as[max_n + 1];
bool book[max_n], fail = false;
vector<int>ans;

void dfs(int bg) {
	int next = as[bg], sum = 1;
	book[bg] = true;
	while (next != bg) {
		if (book[next]) {
			fail = true;
			return;
		}
		sum++;
		book[next] = true;
		next = as[next];
	}
	if (sum % 2 == 0) ans.push_back(sum / 2);
	else ans.push_back(sum);
}

long long gcd(long long x, long long y) {
	long long t;
	while (y != 0) {
		t = x % y;
		x = y;
		y = t;
	}
	return x;
}

long long lcm(long long x, long long y) {
	long long ret = gcd(x, y);
	return x * y / ret;
}

int main() {
	int n, u, v;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> as[i];
	}
	for (int i = 1; !fail && i <= n; i++) {
		if (!book[i]) {
			dfs(i);
		}
	}
	if (fail) {
		cout << -1 << endl;
		return 0;
	}
	long long ret = 1;
	for (auto iter : ans) {
		ret = lcm((long long)(iter), ret);
	}
	cout << ret << endl;
	return 0;
}

T10 [Daimayuan] 树(C++,动态规划,01背包方案数)

有一棵 \(n\) 个节点的以 \(1\) 号点为根的有根树。现在可以对这棵树进行若干次操作,每一次操作可以选择树上的一个点然后删掉连接这个点和它的儿子的所有边。

现在我们想知道对于每一个 \(k\) (\(1≤k≤n\)),最少需要多少次操作能让图中恰好存在 \(k\) 个联通块。

输入格式

第一行输入一个正整数 \(n\)

第二行输入 \(n−1\) 个整数 \(f_1,f_2,...,f_{n−1}\)\(f_i\) 表示 \(i+1\) 号点的父亲,保证 \(1≤f_i≤i\)

输出格式

输出 \(n\) 个整数,第 \(i\) 个数表示 \(k=i\) 时的答案,如果无法让图中恰好存在 \(i\) 个联通块,则输出 -1

样例输入1

6
1 2 1 1 2

样例输出1

0 -1 1 1 -1 2

数据规模

\(10\) 个测试点。

测试点 \(1,2,3\) 满足 \(n≤20\)

测试点 \(4,5,6\) 满足 \(n≤100\)

对于所有数据,满足 \(1≤n≤3000\)

解题思路

对于一棵树来说,删去任意一条边都会使连通块数目\(+1\)

那么要判断能否得到\(k\)个连通块,我们只需要判断能否恰好删去\(k-1\)条边。

题目要求操作为:删除一个节点与子节点之间的所有边。

那么统计每个节点的子节点数目,然后就变为了01背包可行性问题:

每一个节点都是一个物品,问能否恰好装满容量为\(k-1\)的背包?

for (int i = 1; i <= n; i++) {//尝试每一个物品
	for (int j = 0; j < n; j++) {//尝试新的重量组合
		if (j - items[i] >= 0)
        	ans[i][j] = ans[i - 1][j] || ans[i - 1][j - items[i]];
    	else
            ans[i][j] = ans[i - 1][j];
    }
}

以上我们只是检验了可行性问题,但是题目中还有另外一个要求:操作次数最少。

因为在物品组合中没有先后顺序,所以我们可以通过物品组合中的物品数量来确定操作次数。

只有新的操作次数小于旧的操作次数的时候,我们才进行更新。

for (int i = 1; i <= n; i++) {//尝试每一个物品
	for (int j = 0; j < n; j++) {//尝试新的重量组合
        if (j - items[i] >= 0 && ans[i - 1][j] && ans[i - 1][j - items[i]])
        	ans[i][j] = min(ans[i - 1][j], ans[i - 1][j - items[i]] + 1);
    	else if (ans[i - 1][j])
            ans[i][j] = ans[i - 1][j];
        else if (j - items[i] >= 0 && ans[i - 1][j - items[i]])
            ans[i][j] = ans[i - 1][j - items[i]];
    }
}

注:1)以上代码段中,ans中元素的含义发生了变化:可行/不可行 -> 物品数量;

2)为了与不存在的组合(ans[i][j] = 0)相区分,我们为所有存在的组合物品数量添加偏置bias,也就是说,物品数量 = ans[i][j] - bias

以上代码的空间复杂度、时间复杂度均可以接受,可以AC,接下来是优化部分qwq。

因为嫌弃这个算法的空间复杂度,所以我们对其进行优化,压缩到二维数组:

for (int i = 1; i <= n; i++) {//尝试每一个物品
	for (int j = 0; j < n; j++) {//尝试新的重量组合
        if (j - items[i] >= 0 && ans[(i - 1) % 2][j] && ans[(i - 1) % 2][j - items[i]])
        	ans[i % 2][j] = min(ans[(i - 1) % 2][j], ans[(i - 1) % 2][j - items[i]] + 1);
    	else if (ans[(i - 1) % 2][j])
            ans[i % 2][j] = ans[(i - 1) % 2][j];
        else if (j - items[i] >= 0 && ans[(i - 1) % 2][j - items[i]])
            ans[i % 2][j] = ans[(i - 1) % 2][j - items[i]];
    }
}

还是嫌弃?继续压,压缩到一维数组:

for (int i = 1; i <= n; i++) {//尝试每一个物品
	for (int j = n; j >= items[i]; j--) {//尝试新的重量组合
		if (j - items[i] >= 0 && ans[j] && ans[j - items[i]])
            ans[j] = min(ans[j], ans[j - items[i]] + 1);
		else if (ans[j]) ans[j] = ans[j];
        else if (j - items[i] >= 0 && ans[j - items[i]])
            ans[j] = ans[j - items[i]] + 1;
	}
}

然后我们删除一些无用的部分:

for (int i = 1; i <= n; i++) {//尝试每一个物品
	for (int j = n; j >= items[i]; j--) {//尝试新的重量组合
		if (ans[j] && ans[j - items[i]]) ans[j] = min(ans[j], ans[j - items[i]] + 1);
		else if (ans[j - items[i]]) ans[j] = ans[j - items[i]] + 1;
	}
}

嗯,好看多了qwq。

最后,AC代码如下:

#include <iostream>
using namespace std;
const int max_n = 3000;

int ans[max_n + 1], n;
int items[max_n + 1];


int main() {
	cin >> n;
	int fa;
	for (int i = 1; i < n; i++) {
		cin >> fa;
		items[fa]++;
	}

	int bias = 1;
	ans[0] = bias;
	for (int i = 1; i <= n; i++) {//尝试每一个物品
		for (int j = n; j >= items[i]; j--) {//尝试新的重量组合
			if (ans[j] && ans[j - items[i]]) ans[j] = min(ans[j], ans[j - items[i]] + 1);
			else if (ans[j - items[i]]) ans[j] = ans[j - items[i]] + 1;
		}
	}

	cout << 0;
	for (int i = 2; i <= n; i++) {
		cout << ' ' << ans[i - 1] - bias;
	}
	return 0;
}

posted @ 2023-06-09 20:50  WitheredSakura  阅读(38)  评论(0编辑  收藏  举报