AcWing T1427 序列
题目描述:
思路:
注意:由于本人实力不足,本题并不能AC,下面我会给出95分(19 / 20)的思路及代码。
注意到本题中有许多数据具有特殊性质的子任务,因此我们可以通过解决子任务来得分。
观察前两个子任务共十个测试点,它们都具有以下性质:
①每个序列只有两个数字;
②只有一个操作,且操作一定覆盖两个数字。
因此我们可以讨论一下t的两种情况,看一下怎么通过仅if/else语句完成这两个子任务。
(1)t = 1时,为了能够变形为b数组,由于a中的两个数同加同减,所以a中两个数与b中两个数的差值要相等。例如:
a : 1 5
b : 5 9
操作 : 1 1 2
在这个数据中,a中的两个数可以通过同时加1操作4次得到b数组,因此可以实现变形。
(2)t = 2时,我们可以这样理解:
如下图,先对a中两个数进行a[1]次操作二,这样一来,就变成了序列{0, a[2] + a[1]}要变形成为b数组的问题了。根据这个思路,我们
再让“0”的位置上加上b[1],“a[2] + a[1]”的位置上减去b[1],如果此时a[2] == b[2]的话,就可以变形成为b序列。即判断:
a[1] + a[2] - b[1] __ b[2]
即:a[1] + a[2] __ b[1] + b[2],若相等,则可以变形成为b,反之则不能。
根据上述分析,我们便可以写出解决前两个子任务的代码:
if(n == 2 && m == 1){
if(t[1] == 1){
if(a[1] - b[1] == a[2] - b[2]) puts("YES");
else puts("NO");
}
else{
if(a[1] + a[2] == b[1] + b[2]) puts("YES");
else puts("NO");
}
}
现在再来观察第三个子任务。这个子任务中的数据满足以下性质:
①每个序列只有两个数字;
②每个操作都一定会覆盖两个数字。
现在我们来看一下这种情况怎么解决。
设再经过k1次操作一和k2次操作二后,a序列变形成为了b序列。那么则有(假设操作一只做加法,操作二只会再a[2]上做加法,在a[1]上
做减法):
a[1] + k1 - k2 = b[1]
a[2] + k1 + k2 = b[2]
上式减去下式,得:
a[1] - a[2] - 2 * k2 = b[1] - b[2]
整理得:
a[1] - a[2] + b[2] - b[1] = -2 * k2
因此只需要判断等号左边的式子是不是偶数,即可判断出能否完成变形。
故我们可以写出解决这一个子任务的代码:
if((a[1] - a[2] + b[2] - b[1]) % 2 == 0) puts("YES");
else puts("NO");
最后再来看一下第四个子任务。这个子任务中的数据满足以下性质:
①所有操作中只有操作二。
我们画一下图:
如上图,在前面我们已经得出“操作二具有流动性”这一结论了,所以我们可以把所有用操作二的连接起来的点的权值放到一个点上。这样一来,将这些用操作二连接起来的点就形成了一个个连通块,而它们的权值之和存储在它们的父节点上。
看到这里,也应该想到了,我们要用并查集来解决这个子任务。根据前面的结论,只要每一个连通块的权值总和等于b中对应联通的联通块的权值总和,即能成功变形为b序列。即上图中的a(i + j + k) == b(i) + b(j) + b(k)时,能完成变形。
因此,我们可以用并查集存储每一个连通块的父节点,用一个va数组来存储a序列和b序列中每一个连通块的权值总和(下标1到n存储a的权值和,n + 1到2n存储b的权值和)。
那么我们就可以写出代码了:
void merge(int x, int y) { //合并操作,传入的x和y分别是操作二中的u和v
int fx = find(x), fy = find(y); //找到两个点的父节点,直接对父节点进行操作
va[fy] += va[fx], va[fx] = 0; //更新连通块的权值和
p[fx] = fy; //合并
}
for(int i = 1 ; i <= 2 * n ; i ++ ) p[i] = i; //并查集初始化
for(int i = 1 ; i <= n ; i ++ )
va[i] = a[i], va[i + n] = b[i]; //开始时每个序列中有n个连通块,每个连通块都只有一个数,因此需要初始化va数组
for (int i = 1 ; i <= m ; i ++ ) { //构造并查集与
int x = u[i], y = v[i]; //一次操作二中需要合并的两个点
if (find(x) == find(y)) continue; //如果已经合并过了,就不管了
merge(x, y); //对a序列进行更新
merge(x + n, y + n); //对b序列进行更新
}
bool flag = true;
for (int i = 1 ; i <= n ; i ++ ) //判断合法性
if (find(i) == i) //由于连通块的权值和存储在父节点上,所以只需找到父节点
if (va[i] != va[i + n]) { //如果这个连通块在a和b两个序列中权值总和不同,则无法成功变形
puts("NO");
flag = false;
break;
}
if (flag) puts("YES");
全部代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int p[N * 2], va[N * 2];
int a[N], b[N], u[N], v[N], t[N];
int find(int x) {
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
void merge(int x, int y) { //合并操作
int fx = find(x), fy = find(y);
va[fy] += va[fx], va[fx] = 0;
p[fx] = fy;
}
int main() {
int T;
scanf("%d", &T);
while (T -- ) {
scanf("%d%d", &n, &m);
for (int i = 1 ; i <= n * 2 ; i ++ )
p[i] = i;
for (int i = 1 ; i <= n ; i ++ )
scanf("%d", a + i), va[i] = a[i];
for (int i = 1 ; i <= n ; i ++ )
scanf("%d", b + i), va[n + i] = b[i];
for (int i = 1 ; i <= m ; i ++ )
scanf("%d%d%d", t + i, u + i, v + i);
if(n == 2){
if(m == 1){
if(t[1] == 1){
if(a[1] - b[1] == a[2] - b[2]) puts("YES");
else puts("NO");
}
else{
if(a[1] + a[2] == b[1] + b[2]) puts("YES");
else puts("NO");
}
}
else{
if((a[1] - a[2] + b[2] - b[1]) % 2 == 0) puts("YES");
else puts("NO");
}
continue;
}
for (int i = 1 ; i <= m ; i ++ ) {
int x = u[i], y = v[i]; //需要合并的两个点
if (find(x) == find(y))
continue; //如果已经合并过了,就跳过
merge(x, y); //对原数组进行合并
merge(x + n, y + n); //对目标数组进行合并
}
bool flag = true;
for (int i = 1 ; i <= n ; i ++ )
if (find(i) == i)
if (va[i] != va[i + n]) {
puts("NO");
flag = false;
break;
}
if (flag)
puts("YES");
}
return 0;
}
看到这里,你可能想问:前四个子任务不就16个点吗?你不是说能过19个点吗?
其实最后那三个没有任何特殊限制的测试点我也不知道是怎么过的,可能是因为数据比较水罢(
所以如果有大神能AC这道题,还请多多指教!