代码源每日一题Div.1 (101-109)

101 - 二分答案

题目链接

序列顺序与操作无关,所以先对序列从小到大进行排序,然后求一个前缀和。

二分可能的答案 x x x,统计将序列中小于 x x x的所有的数字都加到 x x x所需要的操作数,判断是否大于 k k k。check函数的实现方法是先找到第一个小于 x x x的数字的位置 p o s pos pos,判断 x × p o s − p r e [ p o s ] ≤ k x\times pos - pre[pos] \leq k x×pospre[pos]k是否成立,如果成立则返回true,否则返回false。

时间复杂度 O ( n log ⁡ n log ⁡ n ) O(n\log n \log n) O(nlognlogn)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

LL n, k;
LL a[100050], pre[100050];

bool check(LL x) {
	LL pos = lower_bound(a + 1, a + n + 1, x) - a - 1;
	LL d = x * pos - pre[pos];
	if (d <= k) return true;
	else return false;
}

void main2() {
	cin >> n >> k;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	sort(a + 1, a + n + 1);
	pre[0] = 0;
	for (int i = 1; i <= n; ++i) {
		pre[i] = pre[i - 1] + a[i];
	}
	LL l = 0, r = 1e14, ans = l;
	while (l <= r) {
		LL mid = (l + r) >> 1;
		if (check(mid)) {
			ans = max(ans, mid);
			l = mid + 1;
		}
		else r = mid - 1;
	}
	cout << ans;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}

102 - 最长因子链

题目链接

一条链中的数字必定是有序的,而题目中的序列顺序是无关的,所以我们将这些数从小到大排序。

数据范围很小,所以我们可以采用 O ( n 2 ) O(n^2) O(n2)的dp来做。

我们设dp[i]表示从小到大排序后,从右往左来数,以第i个数为最左端的链的最长长度。

于是有:当 1 ≤ i < j ≤ n , a [ j ] ∣ a [ i ] 1\leq i < j \leq n,a[j]|a[i] 1i<jn,a[j]a[i]时,有 d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i] = max(dp[i], dp[j] + 1) dp[i]=max(dp[i],dp[j]+1)

倒序遍历 i i i即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n;
int a[1050], dp[1050];
int ans = 0;

void main2() {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	sort(a + 1, a + n + 1);
	for (int i = 1; i <= n; ++i) {
		dp[i] = 1;
	}
	for (int i = n - 1; i >= 1; --i) {
		for (int j = i + 1; j <= n; ++j) {
			if (a[j] % a[i] == 0) dp[i] = max(dp[i], dp[j] + 1);
		}
	}
	for (int i = 1; i <= n; ++i) {
		ans = max(ans, dp[i]);
	}
	cout << ans;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}

103 - 子串的最大差

题目链接

题目中求所有子串的最大差(最大值-最小值)之和,我们可以将问题转化为求所有子串的最大值之和、所有子串的最小值之和,然后将所得的两个值做差。

求所有子串的最大值之和与最小值之和的方法是基本相同的。

求所有子串的最大值,其实就等同于看每一个数对于所求值的贡献,看这个数作为最大值的子串的范围,根据范围,求出可以形成的包含这个数的范围内的区间个数。

更简单地来说,如果序列中有一个数x,左边有a个数比x小,右边有b个数比x小,那么x对答案的贡献就是 ( a + 1 ) × ( b + 1 ) × x (a+1)\times (b+1)\times x (a+1)×(b+1)×x,在这些区间内,都可以保证x是最大的,换句话说,我们枚举的这些区间的最大值就是 x x x ( a + 1 ) × ( b + 1 ) (a+1)\times (b+1) (a+1)×(b+1)是区间的个数,相乘即为贡献。

现在求左边/右边比 x x x小的数有多少。这道题目的数据范围是 ( n ≤ 500000 ) (n\leq 500000) (n500000),所以我们需要一种 O ( n ) O(n) O(n)的方法是最好的。不难想到使用单调栈来做这道题。

单调栈内存放的是数字的编号。从第一个数开始加入栈,将单调栈内所指的值小于当前值的编号全部弹出,每弹出一个,就计算被弹出的编号对应的数的范围。将所有的符合条件的编号弹出去之后,将当前值的编号插入。

每弹出一个编号,就可以计算这个编号对应的范围的右端点:为将其弹出的编号的值-1。

每推入一个编号,就可以计算新加的编号对应的范围的左端点:为推入前栈顶的编号+1。

就这样走完每一个数后,我们就获得了 n n n个数对应的范围的左右端点。假设我们的编号是 y y y,得到的左右端点分别是 l [ y ] , r [ y ] l[y],r[y] l[y],r[y],这样第 i i i个数的贡献,就是 a [ i ] × ( l [ i ] + 1 ) × ( r [ y ] + 1 ) a[i]\times (l[i] + 1)\times (r[y] + 1) a[i]×(l[i]+1)×(r[y]+1)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

LL n, ans1, ans2, si;
LL a[500050], l[500050], r[500050], s[500050];

void get_max() {
	si = 0;
	for (int i = 1; i <= n; ++i) {
		l[i] = 0; r[i] = n;
	}
	s[0] = 0;
	for (int i = 1; i <= n; ++i) {
		while (si and a[s[si]] < a[i]) {
			r[s[si]] = i - 1;
			--si;
		}
		l[i] = s[si] + 1;
		s[++si] = i;
	}
//	for (int i = 1; i <= n; ++i) {
//		cout << "i = " << i << " l = " << l[i] << " r = " << r[i] << '\n'; 
//	}
	ans1 = 0;
	for (int i = 1; i <= n; ++i) {
		LL d = (r[i] - i + 1) * (i - l[i] + 1);
		ans1 += (a[i] * d);
	}
}

void get_min() {
	si = 0;
	for (int i = 1; i <= n; ++i) {
		l[i] = 0; r[i] = n;
	}
	s[0] = 0;
	for (int i = 1; i <= n; ++i) {
		while (si and a[s[si]] > a[i]) {
			r[s[si]] = i - 1;
			--si;
		}
		l[i] = s[si] + 1;
		s[++si] = i;
	}
	ans2 = 0;
	for (int i = 1; i <= n; ++i) {
		LL d = (r[i] - i + 1) * (i - l[i] + 1);
		ans2 += (a[i] * d);
	}
}

void main2() {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	get_max();
	get_min();
	cout << ans1 - ans2; 
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}

104 - no crossing

题目链接

原题:CF793D

虽然题目中给的是一个有向图,但这道题目可以按照 x x x轴的一个坐标来考量。因为题目中需要满足的条件是 min ⁡ ( x , y ) ≤ t ≤ max ⁡ ( x , y ) \min(x,y) \leq t\leq \max (x,y) min(x,y)tmax(x,y),是一个和编号有关的条件,把这个条件放在 x x x轴上,那么就是在 x x x轴上从 u u u点移动到 v v v ( u < v ) (u<v) (u<v)的条件是满足 u < t < v u<t<v u<t<v t t t点没有被走到过。这样的话,除了我们最开始向一个方向推进的时候移动的距离长短无所谓以外,一旦有一次折返,那么接下来可以移动的位置就会被限制。也就是说,随着移动的不断发生,可以移动的位置范围越来越小。

也就是说,我们考虑的范围是大区间到小区间。想到区间DP的更新方式是小区间到大区间,那么我们能否把整个过程逆转一下,变成小区间到大区间的过程?如果可以的话,是不是就可以用区间DP来做这一道题了?答案是肯定的。我们只需要枚举每一个可能的路线的终点,按照流程逆着走就好了。这样可以移动的范围就是越来越大的了。

我们设 d p [ i ] [ l ] [ r ] [ k ] dp[i][l][r][k] dp[i][l][r][k]为当前要进行第 i i i次行动,现在目标点的范围在 ( l , r ) (l,r) (l,r) k = 0 k=0 k=0表示当前行动起点在 l l l的位置, k = 1 k=1 k=1表示当前行动起点在 r r r的位置。

现在来球 d p [ i ] [ l ] [ r ] [ 0 ] dp[i][l][r][0] dp[i][l][r][0] d p [ i ] [ l ] [ r ] [ 1 ] dp[i][l][r][1] dp[i][l][r][1]。起初我们将这两个值都初始化为无穷大。

首先要明确的是:我们所求的 d p [ i ] [ l ] [ r ] [ 0 / 1 ] dp[i][l][r][0/1] dp[i][l][r][0/1]并不表示我们所走的路线完全覆盖整个 [ l , r ] [l,r] [l,r]区间,可能只是区间内的某一部分,如区间DP的思想,表征整个区间内的所有情况中的最优情况。

先考虑 d p [ i ] [ l ] [ r ] [ 0 ] dp[i][l][r][0] dp[i][l][r][0]的求解:

我们假设存在一个点 v v v,存在一条有向边 l → v l→v lv,边权设为 w w w,且满足 l < v < r l<v<r l<v<r

此时我们想要填加 l → v l→v lv这条边,那么我们就与题目相逆的过程而言的上一条边有两种情况,如图:

在这里插入图片描述

  1. 如图内①,自区间 [ l , v ] [l,v] [l,v]内部而来。这种情况下,有 d p [ i ] [ l ] [ r ] [ 0 ] = d p [ i − 1 ] [ l ] [ v ] [ 1 ] + w dp[i][l][r][0]=dp[i - 1][l][v][1]+w dp[i][l][r][0]=dp[i1][l][v][1]+w
  2. 如图内②,联系两次同向移动自然不会出现经过已经走过的点的情况,也就是自通向连续移动而来。这种情况下,有 d p [ i ] [ l ] [ r ] [ 0 ] = d p [ i − 1 ] [ v ] [ r ] [ 0 ] + w dp[i][l][r][0]=dp[i-1][v][r][0]+w dp[i][l][r][0]=dp[i1][v][r][0]+w

对于这一个点 v v v对答案的更新,那就是 min ⁡ ( d p [ i − 1 ] [ l ] [ v ] [ 1 ] , d p [ i − 1 ] [ v ] [ r ] [ 0 ] ) + w \min (dp[i-1][l][v][1],dp[i-1][v][r][0])+w min(dp[i1][l][v][1],dp[i1][v][r][0])+w

枚举所有满足条件的 v v v,不断更新 d p [ i ] [ l ] [ r ] [ 0 ] dp[i][l][r][0] dp[i][l][r][0]的最小值。

然后考虑 d p [ i ] [ l ] [ r ] [ 1 ] dp[i][l][r][1] dp[i][l][r][1]的求解:(和上面基本一样)

我们假设存在一个点 v v v,存在一条有向边 r → v r→v rv,边权设为 w w w,且满足 l < v < r l<v<r l<v<r

此时我们想要填加 r → v r→v rv这条边,那么我们就与题目相逆的过程而言的上一条边有两种情况,如图:

在这里插入图片描述

  1. 如图内①,自区间 [ v , r ] [v,r] [v,r]内部而来。这种情况下,有 d p [ i ] [ l ] [ r ] [ 1 ] = d p [ i − 1 ] [ v ] [ r ] [ 0 ] + w dp[i][l][r][1]=dp[i - 1][v][r][0]+w dp[i][l][r][1]=dp[i1][v][r][0]+w
  2. 如图内②,联系两次同向移动自然不会出现经过已经走过的点的情况,也就是自通向连续移动而来。这种情况下,有 d p [ i ] [ l ] [ r ] [ 0 ] = d p [ i − 1 ] [ l ] [ v ] [ 1 ] + w dp[i][l][r][0]=dp[i-1][l][v][1]+w dp[i][l][r][0]=dp[i1][l][v][1]+w

对于这一个点 v v v对答案的更新,那就是 min ⁡ ( d p [ i − 1 ] [ l ] [ v ] [ 1 ] , d p [ i − 1 ] [ v ] [ r ] [ 0 ] ) + w \min (dp[i-1][l][v][1],dp[i-1][v][r][0])+w min(dp[i1][l][v][1],dp[i1][v][r][0])+w

枚举所有满足条件的 v v v,不断更新 d p [ i ] [ l ] [ r ] [ 1 ] dp[i][l][r][1] dp[i][l][r][1]的最小值。

求得所有的 d p [ i ] [ l ] [ r ] [ 0 / 1 ] dp[i][l][r][0/1] dp[i][l][r][0/1]后,我们的最终答案从所有原题过程中可能作为起点的点向左或向右的状态。即:

a n s = min ⁡ { min ⁡ ( d p [ k − 1 ] [ 0 ] [ i ] [ 1 ] , d p [ k − 1 ] [ i ] [ n + 1 ] [ 0 ] ) } , 1 ≤ i ≤ n ans=\min \{ \min(dp[k-1][0][i][1], dp[k-1][i][n+1][0])\},1\leq i\leq n ans=min{min(dp[k1][0][i][1],dp[k1][i][n+1][0])},1in

一共进行了 k − 1 k-1 k1次移动,对于每一个点,向左就是右端点为起点,范围是 ( 0 , i ) (0,i) (0,i),向右就是左端点为起点,范围是 ( i + 1 , n + 1 ) (i+1,n+1) (i+1,n+1)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n, k, m;
int dp[105][105][105][2];
int en = 0;
int front[105];

struct Edge {
	int v, w, next;
}e[4050];

void addEdge(int u, int v, int w) {
	e[++en] = {v, w, front[u]};
	front[u] = en;
}

void main2() {
	cin >> n >> k >> m;
	for (int i = 1; i <= n; ++i) {
		front[i] = 0;
	}
	for (int i = 1; i <= m; ++i) {
		int u, v, w;
		cin >> u >> v >> w;
		addEdge(u, v, w);
	}
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			for (int k = 0; k <= 1; ++k) {
				dp[0][i][j][k] = 0;
			}
		}
	}
	for (int i = 1; i < k; ++i) {
		for (int l = n; l >= 0; --l) {
			for (int r = l + 1; r <= n + 1; ++r) {
				dp[i][l][r][0] = 1e9;
				for (int j = front[l]; j; j = e[j].next) {
					int v = e[j].v, w = e[j].w;
					if (l < v and v < r) {
						dp[i][l][r][0] = min({dp[i][l][r][0], dp[i - 1][l][v][1] + w, dp[i - 1][v][r][0] + w});
					}
				}
				dp[i][l][r][1] = 1e9;
				for (int j = front[r]; j; j = e[j].next) {
					int v = e[j].v, w = e[j].w;
					if (l < v and v < r) {
						dp[i][l][r][1] = min({dp[i][l][r][1], dp[i - 1][l][v][1] + w, dp[i - 1][v][r][0] + w});
					}
				}
			}
		}
	}
	int ans = 1e9;
	for (int i = 1; i <= n; ++i) {
		ans = min({ans, dp[k - 1][0][i][1], dp[k - 1][i][n + 1][0]});
	}
	if (ans >= 1e9) cout << -1;
	else cout << ans;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	int _; _ = 1;
	while (_--) main2();
	return 0;
} 

105 - Dis

题目链接

选择任意一个点作为树的根节点,对树进行一次dfs,对于每一个点,记录其父节点编号和从根节点到这个点的唯一路径上所有点的权值异或和。

对于每一次查询 x , y x,y x,y,先找到两个节点的最近公共祖先,设为 l l l,那么先统计 l → x l→x lx路径上所有点的异或和 p p p,再统计 t → y t→y ty路径上所有点的异或和 q q q,其中 t t t表示 l → y l→y ly路径上父节点为 l l l的点(可能是 y y y本身,也可能根本不存在这样的路径)。那么查询的答案就是 p p p q q q的异或和。

由于是异或和,因为异或本身的性质,这道题目我们不用太考虑 x , y x,y x,y和其最近公共祖先 l l l的关系,因为不管怎么样,多余的一部分都会因为异或了偶数次而抵消掉。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n, m, en = 0; 
int a[200005], front[200005], pre[200005], dep[200005], lg[200005], fa[200005][22];

struct Edge {
	int v, next;
}e[400005];

void addEdge(int u, int v) {
	e[++en] = {v, front[u]};
	front[u] = en;
}

void dfs(int u, int f) {
	dep[u] = dep[f] + 1;
	fa[u][0] = f;
	for (int i = 1; (1 << i) <= dep[u]; ++i) {
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	}
	pre[u] = (pre[f] ^ a[u]);
	for (int i = front[u]; i; i = e[i].next) {
		int v = e[i].v;
		if (v == f) continue;
		dfs(v, u);
	}
}

int lca(int x, int y) {
	if (dep[x] < dep[y]) swap(x, y);
	while (dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]]];
	if (x == y) return x;
	for (int k = lg[dep[x]]; k >= 0; --k) {
		if (fa[x][k] != fa[y][k]) {
			x = fa[x][k]; y = fa[y][k];
		}	
	}
	return fa[x][0];
}

void main2() {
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		front[i] = pre[i] = 0;
	}
	lg[1] = 0; lg[2] = 1;
	for (int i = 3; i <= n; ++i) {
		lg[i] = lg[i / 2] + 1;
	}
	for (int i = 1; i < n; ++i) {
		int u, v;
		cin >> u >> v;
		addEdge(u, v); addEdge(v, u);
	}
	pre[0] = 0;
	dfs(1, 0);
	for (int i = 1; i <= m; ++i) {
		int x, y;
		cin >> x >> y;
		int l = lca(x, y);
		int ans = ((pre[x] ^ pre[fa[l][0]]) ^ (pre[y] ^ pre[l]));
		cout << ans << '\n';
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}

106 - 选数

题目链接

很容易可以想到,序列中的每一个数中只有对 n n n取模的模数部分是存在贡献的,那么我们只需要考虑模数。符合输出条件的几个数的模数之和对 n n n取模后为 0 0 0

本题采取前缀和的思想,即令 f [ i ] f[i] f[i]表示前 i i i个数的前缀和对 n n n取模。这样的话,如果存在 f [ x ] = 0 f[x]=0 f[x]=0,那么就可以说明前 x x x个数的集合就是一个符合题目要求的答案。

如果不存在 f [ x ] = 0 f[x]=0 f[x]=0,那么——根据 f [ i ] f[i] f[i]的定义,我们发现 f [ i ] f[i] f[i]一共只可能有 n − 1 n-1 n1个取值。这说明势必存在两个不相同的数 x , y x,y x,y,满足 f [ x ] = f [ y ] f[x]=f[y] f[x]=f[y]。我们假设 x < y x<y x<y,这样的话就意味着 a x + 1 + a x + 2 + ⋯ + a y ≡ 0 ( m o d n ) a_{x+1}+a_{x+2}+\cdots+a_y ≡0\pmod n ax+1+ax+2++ay0(modn)。这样的话 a x + 1 , a x + 2 , ⋯   , a y a_{x+1},a_{x+2},\cdots , a_y ax+1,ax+2,,ay就是一个可行答案的集合。

可以发现,一定存在一个合法答案。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n; 
int a[100050], pre[100050], f[100050], cnt[100050];

void main2() {
	cin >> n;
	for (int i = 0; i <= n; ++i) {
		cnt[i] = 0;
	}
	pre[0] = 0;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		pre[i] = pre[i - 1] + a[i];
		f[i] = pre[i] % n;
		cnt[f[i]]++;
	}
	if (cnt[0] > 0) {
		for (int i = 1; i <= n; ++i) {
			if (f[i] == 0) {
				cout << i << '\n';
				for (int j = 1; j <= i; ++j) {
					cout << a[j] << ' ';
				}
			}
		}
	}
	else {
		int x;
		for (int i = 1; i < n; ++i) {
			if (cnt[i] > 1) {
				x = i; break;
			}
		}
		int l = -1, r = -1;
		for (int i = 1; i <= n; ++i) {
			if (f[i] == x) {
				if (l == -1) l = i;
				else {
					r = i; break;
				}
			}
		}
		cout << r - l << '\n';
		for (int i = l + 1; i <= r; ++i) {
			cout << a[i] << ' ';
		}
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}

107 - 序列操作

题目链接

我们发现有很多的操作的结果都被后面的操作给覆盖掉了,比如说第 4 4 4次操作将第 2 2 2个位置变成了 5 5 5,第 6 6 6次操作又让所有小于 10 10 10的数变成了 10 10 10,那么这个时候第 4 4 4次的操作就相当于没有做过了。也就是说,这道题目如果我们正着做就会变得很麻烦,但是倒着做,我们如果只去考虑每一个位置的数在什么时候是最后更改的,就好了。

我们观察到,单纯只看第二步操作的话,如果后面的操作数比前面的操作数大,那么前面的操作就是没有用的。所以我们倒着做的时候,记录下当前第二种操作遇到的最大操作数。

如果我们倒着做到了第一种操作,第一种操作是将第 x x x个位置上的数变成 y y y,执行完这一步操作的时候,前面的步骤无论是第一种还是第二种,对于第 x x x个位置的数都是无效操作了。所以当我们遇到第一种操作时,可以操作位置标记,之后再遇到这个位置的修改就直接忽略。

我们设从后往前看步骤时第二种操作的操作数当前遇到的最大值是 m x mx mx。当我们遇到第二种操作时,我们将 m x mx mx与操作数进行比较,如果 m x mx mx比操作数小,就将其变成 m x mx mx,也就是随时更新最大值。当我们遇到第一种操作时,我们先看这个位置有没有被第一种操作修正过,如果没有的话,看当前这个值和 m x mx mx哪一个更大,如果 m x mx mx更大,那么将 m x mx mx赋给这个位置的值,然后将这个位置标记,表明这个位置已经被第一种操作修正过了。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n, q;
int a[1000050], mp[1000050];
int b[1000050][3];

void main2() {
	cin >> n >> q;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		mp[i] = 0;
	}
	for (int i = 1; i <= q; ++i) {
		cin >> b[i][0];
		if (b[i][0] == 1) {
			for (int j = 1; j <= 2; ++j) {
				cin >> b[i][j];
			}
		}
		else cin >> b[i][1];
	}
	int mx = 0;
	for (int i = q; i >= 1; --i) {
		if (b[i][0] == 1) {
			if (!mp[b[i][1]]) a[b[i][1]] = max(mx, b[i][2]);
			mp[b[i][1]] = 1;
		}
		else {
			mx = max(mx, b[i][1]);
		}
	}
	for (int i = 1; i <= n; ++i) {
		if (!mp[i] and a[i] < mx) {
			a[i] = mx;
		}
	}
	for (int i = 1; i <= n; ++i) {
		cout << a[i] << " ";
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}

108 - 数数

题目链接

考虑离线。我们将所有询问按照 H i H_i Hi从大到小排列。整体思路就是,如果查询到 H i H_i Hi时,大于 H i H_i Hi的所有的数都是没有意义的数,我们只会统计区间内有意义的数的数量。

建一棵 [ 1 , n ] [1,n] [1,n]的线段树,线段树的叶子节点对应的单个区间记录这个数是否有意义,有意义则为 1 1 1,无意义则为 0 0 0。区间长度大于 1 1 1的线段结点则只对子区间的结果求和。这样就是每一个区间的值表示区间内有意义的数的数量。初始建树时,每一个叶子结点的值都为 1 1 1

读入数据,将序列的 n n n个值和 m m m H H H,将其离散化,然后对所有询问按照 H i H_i Hi从大到小进行排列,此时的 H i H_i Hi要替换成离散后的结果。然后遍历所有排序后的查询,每次查询前,把大于当前查询值 H H H的所有位置标为没有意义。在查询前用vector存储每一个数在序列中的编号,这样就可以在整个过程中标为没有意义的总时间复杂度时 O ( n ) O(n) O(n),查询询问是 O ( m log ⁡ n ) O(m\log n) O(mlogn)。所以总体的时间复杂度就是 O ( n + m log ⁡ n ) O(n+m\log n) O(n+mlogn)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n, m, bi, si;
int a[100050], b[200005], s[200005];

struct QUERY {
	int id, l, r, h, ans;
}q[100005];

vector<int> v[200005];

struct Tree {
	int l, r, sum;
}t[1000005];

void build_tree(int ni, int l, int r) {
	t[ni].l = l; t[ni].r = r;
	if (l == r) {
		t[ni].sum = 1;
		return;
	}
	int mid = (l + r) >> 1;
	build_tree(ni << 1, l, mid);
	build_tree(ni << 1 | 1, mid + 1, r);
	t[ni].sum = t[ni << 1].sum + t[ni << 1 | 1].sum;
}

void add(int ni, int l, int x) {
	if (l == t[ni].l and t[ni].r == l) {
		t[ni].sum += x;
		return;
	}
	int mid = (t[ni].l + t[ni].r) >> 1;
	if (l <= mid) add(ni << 1, l, x);
	else add(ni << 1 | 1, l, x);
	t[ni].sum = t[ni << 1].sum + t[ni << 1 | 1].sum;
}

int query(int ni, int l, int r) {
	if (l <= t[ni].l and t[ni].r <= r) {
		return t[ni].sum;
	}
	int mid = (t[ni].l + t[ni].r) >> 1;
	int ans = 0;
	if (l <= mid) ans += query(ni << 1, l, r);
	if (mid < r)  ans += query(ni << 1 | 1, l, r);
	return ans;
}

void main2() {
	cin >> n >> m;
	for (int i = 1; i <= si; ++i) {
		v[i].clear();
	}
	bi = si = 0;
	map<int, int> mp;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		s[++si] = a[i];
	}
	for (int i = 1; i <= m; ++i) {
		cin >> q[i].l >> q[i].r >> q[i].h;
		s[++si] = q[i].h;
	}
	sort(s + 1, s + si + 1);
	for (int i = 1; i <= si; ++i) {
		if (!mp[s[i]]) {
			b[++bi] = s[i];
			mp[s[i]] = bi;
		}
	}
	for (int i = 1; i <= n; ++i) {
		v[mp[a[i]]].push_back(i);
	}
	for (int i = 1; i <= m; ++i) {
		int x;
		q[i].h = mp[q[i].h];
		q[i].id = i;	
	}
	sort(q + 1, q + m + 1, [](const QUERY &A, const QUERY &B) {
		return A.h > B.h;
	});
	build_tree(1, 1, n);
	q[0].h = bi;
	for (int i = 1; i <= m; ++i) {
		if (q[i].h < q[i - 1].h) {
			for (int j = q[i - 1].h; j > q[i].h; --j) {
				for (int x: v[j]) {
					add(1, x, -1);
				}
			}
		}
		q[i].ans = query(1, q[i].l, q[i].r);
		
	}
	sort(q + 1, q + m + 1, [](const QUERY &A, const QUERY &B) {
		return A.id < B.id;
	});
	for (int i = 1; i <= m; ++i) {
		cout << q[i].ans << ' ';
	}
	cout << '\n';
}

int main() {
//	ios::sync_with_stdio(false);
//	cin.tie(0); cout.tie(0);
	LL _;
	cin >> _;
//	_ = 1;
	while (_--) main2();
	return 0;
}

109 - Minimum Or Spanning Tree

题目链接
原题:CF888G

想要边权或值最小,那就要每一位都要尽可能不存在边权在这一位上为 1 1 1的边。我们知道,位的权值越高,这一位放 1 1 1答案就会越大,所以我们可以贪心地考虑,从高位到低位(二进制从左到右)去看每一位是放 0 0 0还是放 1 1 1。我们找到所有的边权在这一位为 0 0 0的边,用并查集看看这些边组在一起能不能让图连通。如果可以,那么这一位选择这些边权在这一位为 0 0 0的边一定是最好的,那么接下来往下的那些位,就只需要在这些边里面去找,也就是说把边权在这一位上是 1 1 1的边删掉。如果不可以,那么这一位对答案的贡献就是 2 x − 1 2^{x-1} 2x1,假设这是第 x x x位,然后不删边。

对于每一位,要先处理并查集的数组,然后遍历所有边,所以总体的时间复杂度是 O ( 32 ( n + m ) ) O(32(n+m)) O(32(n+m))

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n, m, en = 0;
int front[200050], fa[200050], rk[200050], mp[400050];

int find(int x) {
	return ((fa[x] == x) ? x : (fa[x] = find(fa[x])));
}
void merge(int i, int j) {
	int x = find(i), y = find(j);
	if (rk[x] <= rk[y]) fa[x] = y;
	else fa[y] = x;
	if (rk[x] == rk[y] && x != y) ++rk[y];
}

struct Edge {
	int u, v, w;
}e[400050];

void main2() {
	cin >> n >> m;
	for (int i = 1; i <= m; ++i) {
		cin >> e[i].u >> e[i].v >> e[i].w;
		mp[i] = 0;
	}
	sort(e + 1, e + m + 1, [](const Edge &A, const Edge &B) {
		return A.w < B.w;
	});
	int ans = 0;
	
	for (int i = 30; i >= 0; --i) {
		for (int j = 1; j <= n; ++j) {
			fa[j] = j; rk[j] = 1;
		}
		for (int j = 1; j <= m; ++j) {
			if (mp[j]) continue;
			if ((e[j].w & (1 << i)) > 0) continue;
			int a = find(e[j].u), b = find(e[j].v);
			if (a != b) merge(a, b);
		}
		int aa = find(1), bk = 0;
		for (int j = 2; j <= n; ++j) {
			int bb = find(j);
			if (aa != bb) {
				bk = 1; break;
			}
		}
		if (bk) ans += (1 << i);
		else {
			for (int j = 1; j <= m; ++j) {
				if (mp[j]) continue;
				if ((e[j].w & (1 << i)) > 0) mp[j] = 1; 
			}
		}
	}
	cout << ans << '\n';
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _;
//	cin >> _;
	_ = 1;
	while (_--) main2();
	return 0;
}
  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值