大三第八周学习笔记

周一

Polygon(思维)

如果最小n-1条边之和小于等于最大边,那么肯定不行,所以至少要满足大于。然后发现大于就一定可以构成多边形。也就是发现一个必要条件,然后发现这也是充分条件

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a) ; i < (b); i++)
#define _for(i, a, b) for(int i = (a) ; i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int a[N], n;

int main()
{
	scanf("%d", &n);
	_for(i, 1, n) scanf("%d", &a[i]);

	sort(a + 1, a + n + 1);
	int sum = 0;
	_for(i, 1, n - 1) sum += a[i];

	puts(sum > a[n] ? "YES" : "NO");

	return 0;
}

Plus(打表)

打了个表发现只有2 3 但是因为数据一大会爆long long,不是很确定

交了一发,wa了,发现是没开long long 然后开long long就过了

不要犯低级错误,交之前检查一遍

Generator(期望dp+打表)

首先很容易写一个期望dp,dp[i]表示当前是i时,到达终点的期望。

于是就可以写一个O(n)的dp,但是题目数据范围是1e9

其实这时可以猜到必然有什么规律

把dp的表打出来,把dp[i] - dp[i - 1]打出来,发现每次增加1/i,也就是说有了可加性

因此1e9可以拆成10个1e8,预处理每一个1e8的答案,对于输入n,把前面多个1e8直接加到答案中,多出来的部分就暴力计算。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

double t[] = {18.9978964139, 0.6931471781, 0.4054651073, 0.2876820720,
0.2231435511, 0.1823215566, 0.1541506797, 0.1335313925, 0.1177830356, 0.1053605156};

int main()
{
	int n; 
	scanf("%d", &n);
	n--;
	
	double ans = 1;
	int cnt = 0, st = 1;
	while(n - st + 1 >= 1e8)
	{
		st += 1e8;
		ans += t[cnt++];
	}

	_for(i, st, n)
		ans += 1.0 / i;
	printf("%.10f\n", ans);

	return 0; 
} 

周二

D. Problem with Random Tests(思维)

赛时是做出来了,但是实现的比较麻烦

看了第一名的代码,用了string的很多特性,比如在长度相同的情况下用字典序直接比较,用find函数,用substr函数,这使得写起来很短,很方便。字符'0'和'1'可以直接或,一个是48一个是49

还有就是要训练在压力下写题的心态,不着急,不乱

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	int n; string s;
	cin >> n >> s;

	if(s.find('1') == string::npos)
	{
		cout << "0" << "\n";
		return 0;
	}
	s = s.substr(s.find('1'));

	if(s.find('0') == string::npos)
	{
		cout << s << "\n";
		return 0;
	}
	int st = s.find('0');

	string ans = s;
	int len = s.size();
	_for(i, 0, st)
	{
		string res = s;
		for(int j = 0; j + i < len; j++)
			res[j + i] |= s[j];
		ans = max(ans, res);
	}
	cout << ans << "\n";

	return 0; 
} 

Maze(bfs)

这题的关键在于,一般bfs中的vis数组都是记录坐标的

但是这道题多了一些东西,坐标并不能代表一个点的状态,还需要记录从哪个方向来,目前走了多少。

这样的时间复杂度是4e6左右,是不会T的。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 110;
int dir[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
int dis[N][N][N][4];
char s[N][N];
int n, m;
struct node
{
	int x, y, step, dir, len;
};

int bfs()
{
	if(s[1][1] != '.' || s[n][n] != '.') return 1e9;
	_for(i, 1, n)
		_for(j, 1, n)
			_for(r, 0, m)
				rep(k, 0, 4)
					dis[i][j][r][k] = 1e9;

	queue<node> q;
	q.push({1, 1, 0, -1, 0});
	while(!q.empty())
	{
		node u = q.front(); q.pop();
		int x = u.x, y = u.y;
		rep(i, 0, 4)
		{
			int xx = x + dir[i][0], yy = y + dir[i][1];
			if(xx < 1 || xx > n || yy < 1 || yy > n || s[xx][yy] != '.') continue;

			node v = {xx, yy, u.step + 1, i, u.dir == i ? u.len + 1 : 1};
			if(v.len > m) continue;
			if(xx == n && yy == n) return v.step;

			if(v.step < dis[xx][yy][v.len][i])
			{
				dis[xx][yy][v.len][i] = v.step;
				q.push(v);
			}
		}
	}
	return 1e9;
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &m);
		_for(i, 1, n) scanf("%s", s[i] + 1);

		int ans = bfs();
		printf("%d\n", ans == 1e9 ? -1 : ans);
	}

	return 0; 
} 

B. Dragon slayer(bfs+状压)

思路比较明显,状压+bfs,但是要注意一些细节

比如用左下角的坐标表示当前这个格子,对于墙需要双向,然后长度需要减去1

坐标范围是0~n-1和0~m-1

当然还有一种方法,二进制枚举+dfs

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 20;
int dir[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
int ban[N][N][N][N], n, m, k;
int xs, ys, xt, yt;
int vis[N][N][1 << 15];
struct node
{
	int x, y, state;
};

void bfs()
{
	_for(i, 0, n)
		_for(j, 0, m)
			rep(S, 0, 1 << k)
				vis[i][j][S] = 0;
	queue<node> q;
	q.push({xs, ys, 0});
	vis[xs][ys][0] = 1;

	while(!q.empty())
	{
		node u = q.front(); q.pop();
		int x = u.x, y = u.y;
		rep(i, 0, 4)
		{
			int xx = x + dir[i][0], yy = y + dir[i][1];
			if(xx < 0 || xx >= n || yy < 0 || yy >= m) continue;

			int state = u.state;
			if(ban[x][y][xx][yy] != -1)
			{
				int cur = ban[x][y][xx][yy];
				if(!((state >> cur) & 1)) state |= 1 << cur;
			}
			if(!vis[xx][yy][state]) 
			{
				q.push({xx, yy, state});
				vis[xx][yy][state] = 1;
			}
		}
	}
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d%d", &n, &m, &k);
		scanf("%d%d%d%d", &xs, &ys, &xt, &yt);

		memset(ban, -1, sizeof ban);
		_for(i, 1, k)
		{
			int x1, y1, x2, y2;
			scanf("%d%d%d%d", &x1, &y1, &x2, &y2);

			if(x1 == x2)
			{
				if(y1 > y2) swap(y1, y2);
				y2--;
				if(x1 > 0)
					_for(j, y1, y2) 
					{
						ban[x1 - 1][j][x1][j] = i - 1;
						ban[x1][j][x1 - 1][j] = i - 1;
					}
					
			}
			else
			{
				if(x1 > x2) swap(x1, x2);
				x2--;
				if(y1 > 0)
					_for(j, x1, x2) 
					{
						ban[j][y1 - 1][j][y1] = i - 1;
						ban[j][y1][j][y1 - 1] = i - 1;
					}
			}
		}

		bfs();

		int ans = 1e9;
		rep(S, 0, 1 << k)
			if(vis[xt][yt][S])
			{
				int cnt = 0;
				rep(i, 0, k)
					if((S >> i) & 1)
						cnt++;
				ans = min(ans, cnt);
			}
		printf("%d\n", ans);
	}

	return 0; 
} 

K. Random(思维)

对于求和,一个数的期望是0.5

而每次一半概率减去最大,一半概率减去最小,这样的话一个数的期望还是0.5

因此答案是(n - m) / 2,求一下逆元

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int mod = 1e9 + 7;

ll binpow(ll a, ll b)
{
	ll res = 1;
	for(; b; b >>= 1)
	{
		if(b & 1) res = res * a % mod;
		a = a * a % mod;
	}
	return res;
}

ll inv(ll x) { return binpow(x, mod - 2); }

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		int n, m; 
		scanf("%d%d", &n, &m);
		printf("%lld\n", (n - m) * inv(2) % mod);
	}

	return 0; 
} 

C. Backpack(背包+bitset优化)

对于枚举j,f[j] |= f[j - w]  可以写成二进制,即f |= f << w用bitset优化

这题就是加了一维而已

写法可以比较简洁,省掉第一维前i个物品

网上有字典树的做法,但是正确性有问题,这种做法每次加入字典树的值是把当前状态的最大值加入,实际上不一定。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = (1 << 10) + 10;
bitset<N> f[N];
int n, m, w[N], v[N];

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &m);
		_for(i, 1, n) scanf("%d%d", &v[i], &w[i]);

		rep(j, 0, 1 << 10)
			f[j].reset();

		f[0][0] = 1;
		_for(i, 1, n)
			rep(j, 0, 1 << 10)
				f[j] |= f[j ^ w[i]] << v[i];
		
		int ans = -1;
		for(int S = (1 << 10) - 1; S >= 0; S--)
			if(f[S][m])
			{
				ans = S;
				break;
			}
		printf("%d\n", ans);
	}

	return 0; 
} 

A. String(kmp树+同余+二分)

首先暴力做法就是一直跳next,每个都判断是否是k的倍数

考虑如何优化,首先建立一颗kmp树,0为根节点

对于每个节点u,需要找它有多少个祖先v,满足

2v-u > 0

2v-u = 0 (mod k)

对于第二个条件,即2v与u同余,那么可以在dfs的过程中用vector存一下不同余数都有哪些点,即在node[2u%k]中加入2u

对于当前点u,查找node[u%k],这里面都是可能的答案

然后要满足里面的点要大于u,那么在vector中二分出一个位置,然后可以算出个数。二分算个数的话vector可以,set不行。这里加入的时候vector本身就是有序的

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int mod = 998244353;
const int N = 1e6 + 10;
vector<int> g[N], node[N];
int ans[N], Next[N], n, k;
char str[N];

void get_next() 
{
	Next[0] = -1;
	int i = 0, j = -1;
	while(i < n)
	{
		if(j == -1 || str[i] == str[j])
		{
			i++; j++;
			Next[i] = j;
			g[Next[i]].push_back(i);
		}
		else j = Next[j];
	}
}

void dfs(int u)
{
	node[2 * u % k].push_back(2 * u);
	int pos = upper_bound(node[u % k].begin(), node[u % k].end(), u) - node[u % k].begin();
	ans[u] = node[u % k].size() - pos;
	for(int v: g[u]) dfs(v);
	node[2 * u % k].pop_back();
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%s%d", str, &k);
		n = strlen(str);
		_for(i, 0, n) g[i].clear();
		get_next();
		
		dfs(0);

		ll res = 1;
		_for(i, 1, n)
		{
			ll cur = ans[i] + 1;
			res = res * cur % mod;
		}
		printf("%lld\n", res);
	}

	return 0; 
} 

还又一种O(n)的做法,即可以用两次kmp求出最长的小于等于一半的串。比赛时是用倍增求这个T了,这个东西其实可以用kmp求,只要保证匹配的时候小于等于一半的串,注意不匹配时是j=Next[j]不是j=Next2[j]

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int mod = 998244353;
const int N = 1e6 + 10;
vector<int> g[N], q[N];
int Next[N], Next2[N], cnt[N], ans1[N], ans2[N], n, k;
char str[N];

void get_next() 
{
	Next[0] = -1;
	int i = 0, j = -1;
	while(i < n)
	{
		if(j == -1 || str[i] == str[j])
		{
			i++; j++;
			Next[i] = j;
			g[Next[i]].push_back(i);
		}
		else j = Next[j];
	}

	Next2[0] = -1;
	i = 0, j = -1;
	while(i < n)
	{
		if((j == -1 || str[i] == str[j]) && 2 * j + 2 <= i + 1)
		{
			i++; j++;
			Next2[i] = j;
			q[Next2[i]].push_back(i);
		}
		else j = Next[j];
	}
}

void dfs(int u)
{
	cnt[2 * u % k]++;
	ans1[u] = cnt[u % k];
	for(int x: q[u]) ans2[x] = cnt[x % k];
	for(int v: g[u]) dfs(v);
	cnt[2 * u % k]--;
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%s%d", str, &k);
		n = strlen(str);
		_for(i, 0, n) g[i].clear(), q[i].clear();
		get_next();
		
		dfs(0);

		ll res = 1;
		_for(i, 1, n)
		{
			ll cur = ans1[i] - ans2[i] + 1;
			res = res * cur % mod;
		}
		printf("%lld\n", res);
	}

	return 0; 
} 

周三

L Alice and Bob(博弈+二进制)

这题Alice和Bob的操作不同,不能用np态来考虑

主要是举几个例子,分析什么时候能赢,找规律

发现一个0Alice赢,两个1Alice赢,一个1两个2Alice赢

这里可以往二进制的方向想,即两个2换成一个1,变成两个1,Alice赢

仔细想一下,Alice可以把数均分成两份,Bob删掉一半,另一半减去1,就相当于a[i - 1] += a[i] / 2

Alice可以一直均分,那么数就越来越小,出现0就赢

所以就可以看作二进制。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int a[N], n;

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d", &n);
		_for(i, 0, n) scanf("%d", &a[i]);

		for(int i = n; i >= 1; i--)
			a[i - 1] += a[i] / 2;
		
		puts(a[0] ? "Alice" : "Bob");
	}

	return 0; 
} 

Capital Program(bfs)

一开始想的是二分答案+树形dp,但是发现一个问题,就是只能统计当前节点的儿子的距离,并不能统计当前节点的父亲距离,然后就卡住了。那么就需要选择一个合适的根,那这个跟该怎么选呢?不好说

其实不应该从一个根dfs,应该从外围往中间bfs,把所有度数为1的点加入队列中,然后往内部bfs。因为bfs是一层一层的,所以每一次肯定是当前答案最好的结果了,所以是对的。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
vector<int> g[N];
int d[N], dis[N], n, k;

int main()
{
	scanf("%d%d", &n, &k);
	_for(i, 1, n - 1)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);
		d[u]++;
		d[v]++;
	}

	queue<int> q;
	_for(i, 1, n)
		if(d[i] == 1)
		{
			dis[i] = 1;
			q.push(i);
		}
	
	int ans = 0, cnt = n;
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		ans = max(ans, dis[u]);
		if(--cnt == k) break;
		for(int v: g[u])
		{
			dis[v] = max(dis[v], dis[u] + 1);
			if(--d[v] == 1) q.push(v);
		}
	}
	printf("%d\n", ans);

	return 0; 
} 

Game(博弈+猜结论)

这题和之前一道题很像,但是不一样的是当前的石子可以移动到很多堆

整体的思路是手推np态,猜结论,证明

推一下,可以猜一下堆奇数是N态,堆偶数的时候,要成对才是P态

在成对的时候,先手操作一堆石子,后手可以操作成对的另一堆石子,然后做一样的操作,这样整体又是成对的,确实是可以的。是符合NP态的,同时全0是终止状态,也是P态

实现的话就是莫队,网上还有一种比较骚的做法就是随机,就是把每个数映射成另一个随机的值,然后用异或和为0

成对的数异或和为0,但是异或和为0不一定成对。

为了避免异或和为0但不成对的情况,将每个数随机映射成一个值,这样几乎就异或和不可能为0了,但这时成对映射后依然成对,异或和依然为0

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
int a[N], s[N], id[N], n;

int main()
{
	srand(time(0));
	_for(i, 1, n) id[i] = rand();

	int n, q;
	scanf("%d%d", &n, &q);
	_for(i, 1, n) scanf("%d", &a[i]), a[i] = id[a[i]];

	_for(i, 1, n) s[i] = s[i - 1] ^ a[i];
	_for(i, 1, q)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		if((r - l + 1) % 2 == 1) puts("Alice");
		else
		{
			int cur = s[r] ^ s[l - 1];
			puts(cur ? "Alice" : "Bob");
		}
	}

	return 0; 
} 

I Laser(思维)

关键是发现一个点肯定在4条直线中的其中一条

随便找一个点,以当前为竖线为例,找一个不在竖线上的点,然后这个点有三种情况得到三个交点,然后判断交点。

然后把图旋转45度再做,重复4次即可

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
int x[N], y[N], n;

bool can(int x, int y)
{
	return !x || !y || x == y || x + y == 0;
}

bool pd(int xx, int yy)
{
	_for(i, 1, n)
		if(!can(x[i] - xx, y[i] - yy))
			return false;
	return true;
}

bool check()   //以当前直线是竖线为例,比较好写
{
	int flag = 1;
	_for(i, 1, n)
	{
		if(x[i] == x[1]) continue;
		flag = 0;
		if(pd(x[1], y[i])) return true;
		if(pd(x[1], y[i] + (x[i] - x[1]))) return true;
		if(pd(x[1], y[i] - (x[i] - x[1]))) return true;
	}
	if(flag) return true;
	return false;
}

bool solve()
{
	_for(t, 1, 4)
	{
		if(check()) return true;
		_for(i, 1, n)
		{
			int tx = x[i], ty = y[i];
			x[i] = tx - ty; y[i] = tx + ty;  //旋转45度
		}
	}
	return false;
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d", &n);
		_for(i, 1, n) scanf("%d%d", &x[i], &y[i]);
		puts(solve() ? "YES" : "NO");
	}

	return 0;
}

P1967 [NOIP2013 提高组] 货车运输(krusal重构树模板)

给一个无向图,每次询问两点之间所有路径种,边权最小值的最大值。

那么肯定是边越大越好,于是用最大生成树重构,这时就变为最大生成树上两点之间唯一路径的最小值。这个可以用krusal重构树求

krusal重构树就是n个节点为叶子,然后遍历边,每个边建立一个新点,点权为边权,然后安装类似哈夫曼树那样合并。最后是一颗二叉树。如果不联通就是二叉树森林。细节上,编号越大的点越可能是根节点,dfs的时候编号从大到小遍历。空间记得开两倍点数

它有一个性质就是两点之间的边权最小值即重构树上两点的LCA的点权,因此求LCA即可

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e4 + 10;
struct Edge
{
	int u, v, w;
	bool operator < (const Edge& rhs) const
	{
		return w > rhs.w;
	}
};
vector<Edge> e;
int f[N], val[N], up[N][25], d[N], cnt, n, m;
vector<int> g[N];

int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }

void dfs(int u, int fa)
{
	d[u] = d[fa] + 1;
	up[u][0] = fa;
	_for(j, 1, 20)
		up[u][j] = up[up[u][j - 1]][j - 1];
	for(int v: g[u]) dfs(v, u);
}

int lca(int u, int v)
{
	if(d[u] < d[v]) swap(u, v);
	for(int j = 20; j >= 0; j--)
		if(d[up[u][j]] >= d[v])
			u = up[u][j];
	if(u == v) return u;
	for(int j = 20; j >= 0; j--)
		if(up[u][j] != up[v][j])
			u = up[u][j], v = up[v][j];
	return up[u][0];
}

int main()
{
	scanf("%d%d", &n, &m);
	_for(i, 1, 2 * n) f[i] = i;
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		e.push_back({u, v, w});
	}
	sort(e.begin(), e.end());

	cnt = n;
	for(auto x: e)
	{
		int u = x.u, v = x.v, w = x.w;
		int fu = find(u), fv = find(v);
		if(fu != fv)
		{
			val[++cnt] = w;
			f[fu] = f[fv] = cnt;
			g[cnt].push_back(fu);
			g[cnt].push_back(fv);
		}
	}

	//注意图可能不联通。然后越大的点越可能是根节点
	for(int i = cnt; i >= 1; i--)
		if(!d[i])
			dfs(i, 0);

	int q; scanf("%d", &q);
	while(q--)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		if(find(u) != find(v)) puts("-1");
		else printf("%d\n", val[lca(u, v)]);
	}

	return 0; 
} 

Life is a Game(krusal重构树+倍增优化)

首先过程就是从起点开始,走一条最短的边,然后不断往外扩展。

这个过程非常符合krusal重构树的过程

从叶子节点开始,如果能跳到父亲就跳,之后当前子树的叶子都能达到。

暴力跳会超时,考虑怎么优化

考虑跳一次,需要满足k+sum[u] >= val[fa]

也就是k >= val[fa] - sum[u]

也就是说,只要k大于等于一个值即可跳,那么如果要连跳两步,那么就是要都大于等于,也就是大于最大值。

于是可以倍增优化,d[i][j]表示从i跳2^j步需要多少,初始化就是上面说的,求数组就取最大值。

然后注意一个细节,向上跳是可能跳到0节点的,要判断一下

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int f[N], a[N], val[N], sum[N], n, m, q, cnt;
int up[N][25], d[N][25];
vector<int> g[N];
struct Edge
{
	int u, v, w;
	bool operator < (const Edge& rhs) const
	{
		return w < rhs.w;
	}
};
vector<Edge> e;

int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }

void dfs(int u, int fa)
{
	up[u][0] = fa;
	_for(j, 1, 20) up[u][j] = up[up[u][j - 1]][j - 1];

	sum[u] = a[u];
	for(int v: g[u])
	{
		dfs(v, u);
		sum[u] += sum[v];
	}
}

void dfs2(int u, int fa)
{
	d[u][0] = val[fa] - sum[u];
	_for(j, 1, 20) d[u][j] = max(d[u][j - 1], d[up[u][j - 1]][j - 1]);
	for(int v: g[u]) dfs2(v, u);
}

int main()
{
	scanf("%d%d%d", &n, &m, &q);
	_for(i, 1, n) scanf("%d", &a[i]);
	_for(i, 1, 2 * n) f[i] = i;
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		e.push_back({u, v, w});
	}
	sort(e.begin(), e.end());

	cnt = n;
	for(auto [u, v, w]: e)
	{
		u = find(u); v = find(v);
		if(u != v)
		{
			val[++cnt] = w;
			f[u] = f[v] = cnt;
			g[cnt].push_back(u);
			g[cnt].push_back(v);
		}
	}

	dfs(cnt, 0);
	dfs2(cnt, 0);

	while(q--)
	{
		int u, k;
		scanf("%d%d", &u, &k);
		for(int j = 20; j >= 0; j--)
			if(k >= d[u][j] && up[u][j])  //这里是有可能跳到0节点的,要去掉
				u = up[u][j];
		printf("%d\n", sum[u] + k);
	}

	return 0;
} 

周四

D. Ball(枚举顺序+bitset优化)

很容易想到枚举两个点ab,dis[a][b]为质数,然后找第三个点c,使得一条边小于等于这个数一条边大于等于这个数

这个看起来是n^3的,如果bitset优化就不会超时

怎么迅速找到边小于当前和大于当前的呢,枚举顺序!

即把边从小到大枚举,那么小于等于当前边的就在之前枚举过了,大于等于当前边的还没有枚举

用bitset表示两个点的边有没有遍历过,那么此时需要一个为0一个为1,因此异或后看有多少个1即可

注意曼哈顿距离会到2e5,我一开始开1e5RE了一发

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a) ; i < (b); i++)
#define _for(i, a, b) for(int i = (a) ; i <= (b); i++)
using namespace std;

typedef long long ll;
const int M = 2e5 + 10;
const int N = 2e3 + 10;
bool vis[M];
vector<int> p;
bitset<N> g[N];
int x[N], y[N], n, m;

struct Edge
{
	int u, v, w;
	bool operator < (const Edge& rhs) const
	{
		return w < rhs.w;
	}
};
vector<Edge> e;

void init()
{
	vis[0] = vis[1] = 1;
	_for(i, 2, 2e5)
	{
		if(!vis[i]) p.push_back(i);
		for(int x: p)
		{
			if(i * x > 2e5) break;
			vis[i * x] = 1;
			if(i % x == 0) break;
		}
	}
}
 
int main()
{
	init();

	int T; scanf("%d", &T);
	while(T--)
	{
		e.clear();
		scanf("%d%d", &n, &m);
		_for(i, 1, n) scanf("%d%d", &x[i], &y[i]), g[i].reset();

		_for(i, 1, n)
			_for(j, i + 1, n)
				e.push_back({i, j, abs(x[i] - x[j]) + abs(y[i] - y[j])});
		sort(e.begin(), e.end());

		ll ans = 0;
		for(auto [u, v, w] : e)
		{
			if(!vis[w]) ans += (g[u] ^ g[v]).count();
			g[u][v] = g[v][u] = 1;
		}
		printf("%lld\n", ans);
	}
 
	return 0;
}

简单瞎搞题(bitset)

看作分组背包,每一组必须选一个。用bitset优化即可。

注意要区分必须选还是可以不选,这两种的dp方程是不一样的

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
bitset<N> f[110];
int n;

int main()
{
	scanf("%d", &n);

	f[0][0] = 1;
	_for(i, 1, n)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		_for(j, l, r) f[i] |= f[i - 1] << (j * j);
	}
	printf("%d\n", f[n].count());

	return 0;
}

H Path(分层图最短路+Set)

走过特殊边后就有一种能力,对于相邻的边,费用可以减k,对于不相邻的边,费用为0

那么可以想到分层图,第一层是正常图,第二层是当前由特殊能力的图。

每次跑完特殊边就可以跳到第二层。

考虑怎么处理不相邻的边费用为0,用一个set维护一个还未得到最短路的点,然后每次遍历这个集合,得到了就删去。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e6 + 10;
struct node
{
	int v; ll w; int ty;
	bool operator < (const node& rhs) const
	{
		return w > rhs.w;
	}
};
vector<node> g[N];
int vis[N], id, n, m, s, k;
ll d[N << 1];

void solve()
{
	set<int> S;
	priority_queue<node> q;
	_for(i, 1, 2 * n) d[i] = 1e18;
	_for(i, 1, n) S.insert(i); //费用为0的边只会跳到普通层
	d[s] = id = 0;
	q.push({s, 0});

	while(!q.empty())
	{
		auto [u, w, ty] = q.top(); q.pop();
		if(u <= n) S.erase(u);
		else S.erase(u - n);

		if(u > n)  //当前在特殊层
		{
			id++;
			for(auto [v, w2, ty2]: g[u - n]) vis[v] = id;
			vector<int> del;
			for(int x: S)
				if(vis[x] != id)
				{
					del.push_back(x);
					d[x] = d[u];      //跑到普通层去
					q.push({x, d[x], 0});
				}
			for(int x: del) S.erase(x);

			for(auto [v, w2, ty2]: g[u - n])
			{
				if(ty2) v += n;
				if(d[u] + w2 - k < d[v])
				{
					d[v] = d[u] + w2 - k;
					q.push({v, d[v], ty2});
				}
			} 
		}
		else
		{
			for(auto [v, w2, ty2]: g[u])
			{
				if(ty2) v += n;
				if(d[u] + w2 < d[v])
				{
					d[v] = d[u] + w2;
					q.push({v, d[v], ty2});
				}
			}
		}
	}
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d%d%d", &n, &m, &s, &k);
		_for(i, 1, n) vis[i] = 0, g[i].clear();
		while(m--)
		{
			int u, v, w, ty;
			scanf("%d%d%d%d", &u, &v, &w, &ty);
			g[u].push_back({v, w, ty});
		}

		solve();

		_for(i, 1, n) 
		{
			ll cur = min(d[i], d[i + n]);
			printf("%lld ", cur == 1e18 ? -1 : cur);
		}
		puts("");
	}

	return 0;
}

P4768 [NOI2018] 归程(krusal重构树)

先考虑是怎么一个过程,显然是先坐车到一个尽可能大的连通块,然后在这个联通块中走最短路。走尽可能大的一个联通块,可以用krusal重构树,即从当前点尽可能往上跳。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 4e5 + 10;
struct node
{
	int v; ll w;
	bool operator < (const node& rhs) const
	{
		return w > rhs.w;
	}
};
vector<node> g[N];
int f[N], val[N], n, m, cnt;
int up[N][25];
ll d[N], s[N];

struct Edge
{
	int u, v; ll w;
	bool operator < (const Edge& rhs) const
	{
		return w > rhs.w;
	}
};
vector<Edge> e;

void solve()
{
	priority_queue<node> q;
	_for(i, 1, n) d[i] = 1e18;
	d[1] = 0;
	q.push({1, d[1]});

	while(!q.empty())
	{
		node x = q.top(); q.pop();
		int u = x.v;
		if(d[u] != x.w) continue;

		for(auto [v, w]: g[u])
			if(d[v] > d[u] + w)
			{
				d[v] = d[u] + w;
				q.push({v, d[v]});
			}
	}
}

int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }

void dfs(int u, int fa)
{
	up[u][0] = fa;
	_for(j, 1, 20) up[u][j] = up[up[u][j - 1]][j - 1];

	if(u > n) s[u] = 1e18;
	else s[u] = d[u];
	for(auto [v, w]: g[u]) 
	{
		dfs(v, u);
		s[u] = min(s[u], s[v]);
	}
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &m);
		e.clear();
		_for(i, 1, 2 * n) g[i].clear(), f[i] = i;
		while(m--)
		{
			int u, v, l, a;
			scanf("%d%d%d%d", &u, &v, &l, &a);
			g[u].push_back({v, l});
			g[v].push_back({u, l});
			e.push_back({u, v, a});
		}

		solve();

		cnt = n;
		sort(e.begin(), e.end());
		_for(i, 1, 2 * n) g[i].clear();
		for(auto [u, v, w]: e)
		{
			u = find(u); v = find(v);
			if(u != v)
			{
				val[++cnt] = w;
				f[u] = f[v] = cnt;
				g[cnt].push_back({u, 0});
				g[cnt].push_back({v, 0});
			}
		}

		dfs(cnt, 0);

		int q, k, S; ll lastans = 0;
		scanf("%d%d%d", &q, &k, &S);
		while(q--)
		{
			int v0, v, p0, p;
			scanf("%d%d", &v0, &p0);
			v = (v0 + k * lastans - 1) % n + 1;
			p = (p0 + k * lastans) % (S + 1);

			for(int j = 20; j >= 0; j--)
				if(val[up[v][j]] > p)
					v = up[v][j];
			printf("%lld\n", lastans = s[v]);
		}
	}

	return 0;
}

周五

P7834 [ONTAK2010] Peaks 加强版(krusal重构树+主席树+dfs序)

krusal重构树主要用到两个性质,一个是两点的lca原图生成树中两点路径的最大/最小边权,一个是在重构树上从叶子节点往上跳,给一个x,如果父亲节点权值小于等于x就可以跳,最后跳到一个节点,其对应子树的叶子节点,就是在原图中走边权不超过x的边所能走到的最大联通分量。

这题要需要求这个联通分量的第k大,这就是主席树经典问题了,还需要用dfs序转化到区间上。

对dfs序建立主席树,每次询问一个区间。

这题其实就是询问子树第k大的值+krusal重构树,两题套在了一起

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int a[N], f[N], val[N], siz[N], n, m, q, cnt;
int up[N][25], L[N], R[N], p[N], idx;
int ls[N << 4], rs[N << 4], s[N << 4], root[N << 4];
struct Edge
{
	int u, v, w;
	bool operator < (const Edge& rhs) const
	{
		return w < rhs.w;
	}
};
vector<Edge> e;
vector<int> g[N];

int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }

void dfs(int u, int fa)
{
	up[u][0] = fa;
	_for(j, 1, 20) up[u][j] = up[up[u][j - 1]][j - 1];
	L[u] = ++idx;
	p[idx] = u;
	if(!g[u].size()) siz[u] = 1;

	for(int v: g[u]) dfs(v, u), siz[u] += siz[v];

	R[u] = idx;
}

void add(int pre, int& k, int l, int r, int x)
{
	k = ++idx;
	ls[k] = ls[pre]; rs[k] = rs[pre]; s[k] = s[pre] + 1;
	if(l == r) return;
	int m = l + r >> 1;
	if(x <= m) add(ls[pre], ls[k], l, m, x);
	else add(rs[pre], rs[k], m + 1, r, x);
}

int ask(int pre, int k, int l, int r, int num)
{
	if(l == r) return l;
	int x = s[rs[k]] - s[rs[pre]], m = l + r >> 1;
	if(num <= x) return ask(rs[pre], rs[k], m + 1, r, num);
	else return ask(ls[pre], ls[k], l, m, num - x);
}

int main()
{
	scanf("%d%d%d", &n, &m, &q);
	_for(i, 1, n) scanf("%d", &a[i]);
	_for(i, 1, 2 * n) f[i] = i;
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		e.push_back({u, v, w});
	}
	
	cnt = n;
	sort(e.begin(), e.end());
	for(auto [u, v, w]: e)
	{
		u = find(u); v = find(v);
		if(u != v)
		{
			val[++cnt] = w;
			f[u] = f[v] = cnt;
			g[cnt].push_back(u);
			g[cnt].push_back(v);
		}
	}

	dfs(cnt, 0);

	idx = 0;
	_for(i, 1, 2 * n - 1) 
	{
		if(a[p[i]]) add(root[i - 1], root[i], 1, 1e9, a[p[i]]);
		else root[i] = root[i - 1];
	}

	int lastans = 0;
	while(q--)
	{
		int u, x, k;
		scanf("%d%d%d", &u, &x, &k);
		u = (u ^ lastans) % n + 1;
		x = x ^ lastans;
		k = (k ^ lastans) % n + 1;

		for(int j = 20; j >= 0; j--)
			if(up[u][j] && val[up[u][j]] <= x)
				u = up[u][j];
		if(siz[u] < k)
		{
			puts("-1");
			lastans = 0;
		}
		else printf("%d\n", lastans = ask(root[L[u] - 1], root[R[u]], 1, 1e9, k));
	}

	return 0;
}

J. Circular Billiard Table(思维)

算出圆形角是2倍角,那么和360求一下最小公倍数即可

对于分数,先把分母乘掉再计算

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;

ll gcd(ll a, ll b) { return !b ? a : gcd(b, a % b); }
ll lcm(ll a, ll b) { return a * b / gcd(a, b); }

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		ll a, b;
		scanf("%lld%lld", &a, &b);

		ll g = gcd(a, b);
		a /= g; b /= g;

		ll ans = b;
		ans *= lcm(2 * a, 360) / (2 * a);

		printf("%lld\n", ans - 1);
	}

	return 0;
}

D. Period(kmp树+倍增)

问一个点被禁掉后还有多少个board。要遍历所有的board就是从长度n开始一直取next。为了加速,可以倍增,即迅速跳到一个离被禁的点最近的点,然后从这里开始有多少board就是答案。在kmp树上倍增即可。因为对board的限制是双向的,也就是说离边界最近的,如果在左边那就在左边,在右边就转化到左边。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int Next[N], d[N], up[N][25], n;
char s[N];
vector<int> g[N];

void get_next()
{
	Next[0] = -1;
	int i = 0, j = -1;
	while(i < n)
	{
		if(j == -1 || s[i] == s[j])
		{
			i++; j++;
			Next[i] = j;
			g[Next[i]].push_back(i);
		}
		else j = Next[j];
	}
}

void dfs(int u, int fa)
{
	up[u][0] = fa;
	_for(j, 1, 20) up[u][j] = up[up[u][j - 1]][j - 1];
	d[u] = d[fa] + 1;
	for(int v: g[u]) dfs(v, u);
}

int main()
{
	scanf("%s", s);
	n = strlen(s);
	get_next();

	dfs(0, 0);

	int q; scanf("%d", &q);
	while(q--)
	{
		int x; scanf("%d", &x);
		if(n - x + 1 < x) x = n - x + 1;

		int u = n;
		for(int j = 20; j >= 0; j--)
			if(up[u][j] >= x)
				u = up[u][j];
		u = up[u][0];
		printf("%d\n", d[u] - 1);
	}

	return 0;
}

后面看了别人的做法发现做麻烦了,直接求前缀和即可。即把所有的board的标记下来

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
 
const int N = 1e6 + 10;
int Next[N], sum[N], n;
char s[N];
 
void get_next()
{
	Next[0] = -1;
	int i = 0, j = -1;
	while(i < n)
	{
		if(j == -1 || s[i] == s[j])
		{
			i++; j++;
			Next[i] = j;
		}
		else j = Next[j];
	}
}
 
int main()
{
	scanf("%s", s);
	n = strlen(s);
	get_next();
 
	int x = n;
	while(x)
	{
		sum[x]++;
		x = Next[x];
	}
	_for(i, 1, n) sum[i] += sum[i - 1];
 
	int q; scanf("%d", &q);
	while(q--)
	{
		int x; scanf("%d", &x); 
		if(n - x + 1 < x) x = n - x + 1;
		printf("%d\n", sum[x - 1]);
	}
 
	return 0;
}

G. Shinyruo and KFC(暴力+复杂度计算)

这题关键是利用ai的求和是小于等于1e5,这意味着两两不同的ai只有根号1e5,相同的数可以一起算,用快速幂。

然后注意计算组合数时,阶乘的逆元要预处理,不要现场算会T

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int mod = 998244353;
const int N = 1e5 + 10;
int n, m;
map<int, int> mp;
ll f[N], invf[N];

ll binpow(ll a, ll b)
{
	ll res = 1;
	for(; b; b >>= 1)
	{
		if(b & 1) res = res * a % mod;
		a = a * a % mod;
	}
	return res;
}

ll inv(ll x) { return binpow(x, mod - 2); }

ll C(ll n, ll m)
{
	if(m > n) return 0;
	return f[n] * invf[m] % mod * invf[n - m] % mod;
}

int main()
{
	f[0] = 1;
	_for(i, 1, 1e5) f[i] = f[i - 1] * i % mod;
	invf[(int)1e5] = inv(f[(int)1e5]);
	for(int i = 1e5 - 1; i >= 0; i--) invf[i] = invf[i + 1] * (i + 1) % mod;

	scanf("%d%d", &n, &m);
	_for(i, 1, n)
	{
		int x; scanf("%d", &x);
		mp[x]++;
	}

	vector<pair<int, int>> ve;
	for(auto x: mp) ve.push_back(x);

	_for(i, 1, m)
	{
		ll res = 1;
		for(auto [x, cnt] : ve)
			res = res * binpow(C(i, x), cnt) % mod;
		printf("%lld\n", res);
	}

	return 0;
}

周六

P2762 太空飞行计划问题(最大权闭合子图)

给一个有向图,每个点有点权。一个点选了它的后继必须选,则称为闭合的,要最大化点权,那就是最大权闭合子图,是一个经典问题

解决方法是网络流,汇点向所有正权点连容量为权值的边,所有负权点向汇点连容量为权值的边,原图的边保留,容量为正无穷。

则正点权和-最小割就是答案

怎么理解呢,我们首先把全部正权点选了,现在考虑怎么删去一些节点

减去的值=正权点不选的值+负权点的值,也恰好是两侧边的容量。

如果是s和t不联通,表示所有选择的正权点,它的后继的边都被割断了,也就是价值被剪掉了,符合条件。

也就是说s和t不联通是符合题目条件的,那么就要删最少的边使得s和t不联通,那就是说花最小的代价满足题目的限制关系。也就是最小割。

怎么输出方案呢,考虑最后一次找增广路径,在残量网络中,还存在的边是还没有被割的边,那么在分层时d数组是有值的,那么只要考虑d数组即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 110;
struct Edge { int from, to, flow; };
vector<Edge> edge;
vector<int> g[N];
int d[N], cur[N], a[N][N];
int sum, n, m, s, t;

void add(int from, int to, int flow)
{
	edge.push_back(Edge{from, to, flow});
	g[from].push_back(edge.size() - 1);
	edge.push_back(Edge{to, from, 0});
	g[to].push_back(edge.size() - 1);
}

bool bfs()
{
	memset(d, 0, sizeof(d));
	queue<int> q;
	q.push(s);
	d[s] = 1;

	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(auto x: g[u])
		{
            Edge e = edge[x];
            if(!d[e.to] && e.flow)
            {
                d[e.to] = d[u] + 1;
                q.push(e.to);
            }
		}
	}

	return d[t];
}

ll dfs(int u, ll in)
{
	if(u == t) return in;
	ll out = 0;
	for(int& i = cur[u]; i < g[u].size(); i++)
		{
			Edge& e = edge[g[u][i]];
			if(d[u] + 1 == d[e.to] && e.flow)
			{
			    ll f = dfs(e.to, min((ll)e.flow, in));
				e.flow -= f;
				edge[g[u][i] ^ 1].flow += f;
				out += f; in -= f;
				if(in == 0) break;
			}
		}
	return out;
}

void build()
{
    scanf("%d%d", &m, &n);
    s = 0; t = m + n + 1;

    _for(i, 1, m)
    {
        int x; scanf("%d", &x);
        sum += x;
        add(s, i, x);

        char tools[10000];
        memset(tools, 0, sizeof tools);
        cin.getline(tools, 10000);
        int ulen = 0, tool;
        while (sscanf(tools + ulen, "%d", &tool)==1)
        {
            add(i, tool + m, 1e9);
            a[i][tool] = 1;

            if(tool == 0) ulen++;
            else while(tool) tool /= 10, ulen++;
            ulen++;
        }
    }
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        add(i + m, t, x);
    }
}

int main()
{
	build();

	int ans = 0;
	while(bfs())
	{
	    memset(cur, 0, sizeof(cur));
        ans += dfs(s, 1e18);
	}

    _for(i, 1, m) if(d[i]) printf("%d ", i); puts("");
    _for(i, 1, n) if(d[i + m]) printf("%d ", i); puts("");
    printf("%d\n", sum - ans);

	return 0;
}

Luxury cruise ship(思维)

365肯定是越多越好,所以考虑枚举365的个数,剩下7和31用dp

打个表可以发现,对于31和7,只要超过200就一定能表示

所以365的个数要不是取到最大,要不是少一个

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e3 + 10;
int f[N];

int main()
{
	memset(f, 0x3f, sizeof f);
	f[0] = 0;

	int w[] = {7, 31};
	_for(i, 1, 1e3)
		rep(j, 0, 2)
			if(i - w[j] >= 0)
				f[i] = min(f[i], f[i - w[j]] + 1);
	
	int T; scanf("%d", &T);
	while(T--)
	{
		ll x; scanf("%lld", &x);

		if(x <= 365) 
		{
			printf("%d\n", f[x] > 1e9 ? -1 : f[x]);
			continue;
		}

		if(f[x % 365] <= 1e9) printf("%lld\n", x / 365 + f[x % 365]);
		else printf("%lld\n", x / 365 - 1 + f[(x % 365) + 365]);
	}

	return 0;
}

Static Query on Tree(树剖+线段树染色)

树剖很久没写了,vp没回忆起来。

看作dfs序的扩展,除了dfs序维护子树区间,树剖可以dfs序维护x到y的路径。同时涉及到子树和路径,就可以用树剖。

C操作对应子树,AB操作对应点到根的路径。那么接下来就是一个染色问题,问有多少点同时染了三种颜色,那么就是线段树染色了。

比较坑的是每次询问过后要清空线段树,为了保证线段树,就把前面全部的修改操作重复一遍,只是改成清空操作,但是注意修改操作的时候会push_down,那么清空的操作也要push_down,即把左右儿子清0,我写完后170多行,但其实就错了这个点,卡了很久,忘了清空的时候也push_down

#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int ta[N << 2], tab[N << 2], tabc[N << 2];
int lazya[N << 2], lazyb[N << 2], lazyc[N << 2];
int d[N], fa[N], top[N], id[N], siz[N], son[N], cnt;
vector<int> g[N];
vector<pair<int, int>> A, B, C;
int n, q;

void dfs1(int u, int father)
{
	d[u] = d[father] + 1;
	fa[u] = father;
	siz[u] = 1;
	for(int v: g[u])
	{
		dfs1(v, u);
		siz[u] += siz[v];
		if(siz[son[u]] < siz[v]) son[u] = v;
	}
}

void dfs2(int u, int fa, int t)
{
	top[u] = t;
	id[u] = ++cnt;
	
	if(siz[u] == 1) return;
	dfs2(son[u], u, t);

	for(int v: g[u])
	{
		if(v == fa || v == son[u]) continue;
		dfs2(v, u, v);
	}
}

void add(int u, int v, int op)
{
	while(top[u] != top[v])
	{
		if(d[top[u]] < d[top[v]]) swap(u, v);
		if(!op) A.push_back({id[top[u]], id[u]});
		else B.push_back({id[top[u]], id[u]});
		u = fa[top[u]];
	}
	if(d[u] < d[v]) swap(u, v);
	if(!op) A.push_back({id[v], id[u]});
	else B.push_back({id[v], id[u]});
}

void updateA(int k, int l, int r)
{
	ta[k] = r - l + 1;
	lazya[k] = 1;
}

void updateB(int k, int l, int r)
{
	tab[k] = ta[k];
	lazyb[k] = 1;
}

void updateC(int k, int l, int r)
{
	tabc[k] = tab[k];
	lazyc[k] = 1;
}

void up(int k)
{
	ta[k] = ta[l(k)] + ta[r(k)];
	tab[k] = tab[l(k)] + tab[r(k)];
	tabc[k] = tabc[l(k)] + tabc[r(k)];
}

void down(int k, int l, int r)
{
	int m = l + r >> 1;
	if(lazya[k]) updateA(l(k), l, m), updateA(r(k), m + 1, r), lazya[k] = 0; 
	if(lazyb[k]) updateB(l(k), l, m), updateB(r(k), m + 1, r), lazyb[k] = 0; 
	if(lazyc[k]) updateC(l(k), l, m), updateC(r(k), m + 1, r), lazyc[k] = 0; 
}

void change(int k, int l, int r, int L, int R, int op)
{
	if(L <= l && r <= R)
	{
		if(!op) updateA(k, l, r);
		else if(op == 1) updateB(k, l, r);
		else updateC(k, l, r);
		return;
	}
	down(k, l, r);
	int m = l + r >> 1;
	if(L <= m) change(l(k), l, m, L, R, op);
	if(R > m) change(r(k), m + 1, r, L, R, op);
	up(k);
}

void cla(int k)
{
	ta[k] = tab[k] = tabc[k] = lazya[k] = lazyb[k] = lazyc[k] = 0;
}

void Clear(int k, int l, int r, int L, int R)
{
	if(L <= l && r <= R)
	{
		cla(k);
		return;
	}
	cla(l(k)); cla(r(k));
	int m = l + r >> 1;
	if(L <= m) Clear(l(k), l, m, L, R);
	if(R > m) Clear(r(k), m + 1, r, L, R);
}
 
int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &q);
		_for(i, 1, n) g[i].clear(), son[i] = d[i] = top[i] = fa[i] = id[i] = siz[i] = 0;
		cnt = 0;
		_for(i, 2, n)
		{
			int x; scanf("%d", &x);
			g[x].push_back(i);
		}

		dfs1(1, 0);
		dfs2(1, 0, 1);

		while(q--)
		{
			A.clear(); B.clear(); C.clear();

			int na, nb, nc, x;
			scanf("%d%d%d", &na, &nb, &nc);
			while(na--)
			{
				scanf("%d", &x);
				add(1, x, 0);
			}
			while(nb--)
			{
				scanf("%d", &x);
				add(1, x, 1);
			}
			while(nc--)
			{
				scanf("%d", &x);
				C.push_back({id[x], id[x] + siz[x] - 1});
			}

			for(auto [l, r]: A) change(1, 1, n, l, r, 0);
			for(auto [l, r]: B) change(1, 1, n, l, r, 1);
			for(auto [l, r]: C) change(1, 1, n, l, r, 2);
			printf("%d\n", tabc[1]);

			for(auto [l, r]: A) Clear(1, 1, n, l, r);
			for(auto [l, r]: B) Clear(1, 1, n, l, r);
			for(auto [l, r]: C) Clear(1, 1, n, l, r);
		}
	}

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值