代码源每日一题Div.1 (601-607)

601 - 并行排序

题目链接

我们发现,对于一个序列,随便选取一个递减的子序列(可以不在原序列连续),那么他们之间连的边,不会因为插入别的点就变化。如果我们选取一个长度为 l l l的递减子序列,那么这个子序列产生的答案(填的颜色的最小值)就是 l l l

而不同的子序列之间,我们可以认为他们没有任何关系(因为只考虑涂哪几种,不考虑涂哪一个),所以,我们只需要考虑最长的下降子序列,就是这道题目的答案了。

#include
using namespace std;
typedef long long LL;

int n;
int a[1000005], dp[1000005];

struct Tree {
	LL l, r, mx;
}t[5000005];

void pu(int ni) {
	t[ni].mx = max(t[ni << 1].mx, t[ni << 1 | 1].mx);
}

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

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

LL query(int ni, int l, int r) {
	if (l <= t[ni].l and t[ni].r <= r) {
		return t[ni].mx;
	}
	int mid = (t[ni].l + t[ni].r) >> 1;
	LL ans1 = 0, ans2 = 0;
	if (l <= mid) ans1 = query(ni << 1, l, r);
	if (mid < r) ans2 = query(ni << 1 | 1, l, r);
	return max(ans1, ans2);
}

void main2() {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		dp[i] = 0;
	}
	build_tree(1, 1, n);
	for (int i = 1; i <= n; ++i) {
		LL tmp = query(1, a[i] + 1, n);
		dp[i] = tmp + 1;
		fix(1, a[i], dp[i]);
	}
	cout << *max_element(dp + 1, dp + n + 1) << '\n';
}

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

602 - 丹钓战

题目链接

参考了严格鸽的题解

本来以为是经典的H-H的项链的问题,但是舒爽写完,难受超时。

大体思路相似,但是有更巧妙的方法:

模拟单调栈的运行过程,掌握每一个元素入栈后,在栈里上一个元素是什么,记为 p [ i ] p[i] p[i]。对于每一个区间 [ L , R ] [L,R] [L,R],我们要查询的就是,区间内的元素的 p [ i ] p[i] p[i] p [ L ] p[L] p[L]的大小关系。如果 p [ i ] > p [ L ] p[i]>p[L] p[i]>p[L],那么就不是成功的元素。即,统计区间内 p [ i ] ≤ p [ L ] p[i]\leq p[L] p[i]p[L] i i i的个数。

接下来统计这个信息的方法尤为关键,因为一旦选取的方法不合适(比如说我),就会时间复杂度起飞。

求区间内不大于 k k k的元素个数,是树状数组经典应用。我们先观察我们对树状数组的所有询问:对于每一个区间 [ L , R ] [L,R] [L,R],考察区间内部的 p [ i ] p[i] p[i]小于等于 p [ L ] p[L] p[L]的元素的个数。对于一个区间,我们要考察的就是那个时候的状态下 p r e [ R ] − p r e [ L − 1 ] pre[R]-pre[L-1] pre[R]pre[L1],那时的树状数组中应当只记录了 p [ i ] ≤ p [ L ] p[i]\leq p[L] p[i]p[L]的元素的数量。

我们按照流程从左到右向单调栈逐渐推入我们的每一个元素,然后通过 p [ i ] p[i] p[i]更新树状数组。对于每一个落在 i i i位置上的询问,我们向对应的答案输送当前情况下的查询结果。这样的总体时间复杂度就是 O ( n log ⁡ n + q ) O(n\log n+q) O(nlogn+q)

#include
using namespace std;
typedef long long LL;
const int N = 500005;

struct QUERY {
	int st, x, id;
};

struct PAIR {
	int x, y;
}a[N];

int n, Q, si;
int c[N], p[N], s[N], ans[N];
vector<QUERY> q[N];

int lowbit(int x) {
	return x & (-x);
}

void add(int x, int k) {
	while (x <= n) {
		c[x] += k; x += lowbit(x);
	}
}

int presum(int x) {
	int ans = 0;
	while (x >= 1) {
		ans += c[x]; x -= lowbit(x);
	}
	return ans;
}

int query(int l, int r) {
	return presum(r) - presum(l - 1);
}

void main2() {
	cin >> n >> Q;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i].x;
		q[i].clear();
	}
	for (int i = 1; i <= n; ++i) {
		cin >> a[i].y;
		p[i] = c[i] = ans[i] = 0;
	}
	si = s[0] = 0;
	for (int i = 1; i <= n; ++i) {
		while (si and (a[i].x == a[s[si]].x or a[i].y >= a[s[si]].y)) {
			--si;
		}
		p[i] = s[si] + 1;
		s[++si] = i;
	}
	for (int i = 1; i <= Q; ++i) {
		int x, y; cin >> x >> y;
		q[x - 1].push_back({-1, p[x], i});
		q[y].push_back({1, p[x], i});
	}
	for (int i = 1; i <= n; ++i) {
		add(p[i], 1);
		for (auto [x, y, z]: q[i]) {
			ans[z] += x * presum(y);
		}
	}
	for (int i = 1; i <= Q; ++i) {
		cout << ans[i] << '\n';
	}
}

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

错误的实现过程

在我不断超时的方法中,我的方法是按照H-H的项链的题目的做法,将 p [ i ] p[i] p[i]求完,然后在树状数组将 p [ i ] p[i] p[i]位置++。然后将所有查询按照右端点从大到小排序,每移动一次右端点,将右端点所经过的所有点通过 p [ i ] p[i] p[i]对树状数组的贡献删去。这样的话,对于每一个区间 [ L , R ] [L,R] [L,R],清楚完之前的贡献之后,答案就是 R − L + 1 − q u e r y ( L , R ) R-L+1-query(L,R) RL+1query(L,R)

看上去也是 O ( n log ⁡ n ) O(n\log n) O(nlogn),但是常数巨大无比。

错误代码如下:

#include
using namespace std;
typedef long long LL;

int n, Q, si;
int a[500005], b[500005], c[500005], p[500005], s[500005], ans[500005];

inline LL read() {
	LL x = 0, y = 1; char c = getchar(); 
	while (c > '9' || c < '0') { if (c == '-') y = -1; c = getchar(); }
	while (c>='0'&&c<='9') { x=x*10+c-'0';c=getchar(); } return x*y;
}

int lowbit(int x) {
	return x & (-x);
}

void add(int x, int k) {
	while (x <= n) {
		c[x] += k; x += lowbit(x);
	}
}

int presum(int x) {
	int ans = 0;
	while (x >= 1) {
		ans += c[x]; x -= lowbit(x);
	}
	return ans;
}

int query(int l, int r) {
	return presum(r) - presum(l - 1);
}

struct QUERY {
	int l, r, id;
}q[500005];

void main2() {
	n = read(), Q = read();
	for (int i = 1; i <= n; ++i) {
		a[i] = read();
	}
	for (int i = 1; i <= n; ++i) {
		b[i] = read();
	}
	si = s[0] = 0;
	for (int i = 1; i <= n; ++i) {
		while (si and (a[i] == a[s[si]] or b[i] >= b[s[si]])) {
			--si;
		}
		p[i] = s[si];
		s[++si] = i;
	}
	for (int i = 1; i <= Q; ++i) {
		q[i].l = read(); q[i].r = read();
		q[i].id = i;
	}
	for (int i = 1; i <= n; ++i) {
		if (p[i] > 0) add(p[i], 1);
	}
	sort(q + 1, q + Q + 1, [](const QUERY &A, const QUERY &B) {
		return A.r > B.r;
	}); 
	int cur = n;
	for (int i = 1; i <= Q; ++i) {
		if (q[i].r < cur) {
			for (int j = cur; j > q[i].r; --j) {
				add(p[j], -1);
			}
			cur = q[i].r;
		}
		ans[q[i].id] = (q[i].r - q[i].l + 1) - query(q[i].l, q[i].r);
	}
	for (int i = 1; i <= Q; ++i) {
		printf("%d\n", ans[i]);
	}
}

int main() {
	LL _ = 1;
//	cin >> _;
	while (_--) main2();
	return 0;
}

603 - 删数

题目链接

参考了严格鸽的文章

考虑题目中操作的性质,可见,对于原序列的差分数组,我们是对两个相等的数进行合并,即如果原差分数组为1 2 2 4,那么经过一次操作后变成1 4 4,经过第二次操作后变成1 8。

显然在差分数组上的考虑是更容易的。我们发现,能够合并的两个数,如果把他们改写成 a × 2 b a\times 2^b a×2b c × 2 d c\times 2^d c×2d,那么一定有 a = c a=c a=c

所以我们令 c n t [ i ] cnt[i] cnt[i]表示差分数组 d [ i ] = a × 2 b d[i]=a\times 2^b d[i]=a×2b中的 b b b,其中 a a a是奇数,再令 d [ i ] = a d[i]=a d[i]=a d [ i ] = 0 d[i]=0 d[i]=0的情况特殊考虑。

g [ i ] [ j ] g[i][j] g[i][j]为在区间 [ i , g [ i ] [ j ] ) [i,g[i][j]) [i,g[i][j])内差分数组可以合并为 d [ i ] × 2 j d[i]\times 2^j d[i]×2j的形式。如果不能合并成 d [ i ] × 2 j d[i]\times 2^j d[i]×2j的形式,则 g [ i ] [ j ] = − 1 g[i][j]=-1 g[i][j]=1。我们在最初的初始化当中,将所有 g [ i ] [ j ] g[i][j] g[i][j]设为 − 1 -1 1

g [ i ] [ j ] g[i][j] g[i][j]的转移,我们考虑 [ i , g [ i ] [ j ] ) [i,g[i][j]) [i,g[i][j])能否达成,就要考虑 [ i , g [ i ] [ j − 1 ] ) [i,g[i][j-1]) [i,g[i][j1])的区间能否合并,若 k = g [ i ] [ j − 1 ] k=g[i][j-1] k=g[i][j1],那么同时也要判断 [ k , g [ k ] [ j − 1 ] ) [k,g[k][j-1]) [k,g[k][j1])能否合并。这两个区间都能合并时,只有 d [ i ] d[i] d[i] d [ g [ i ] [ j ] ] d[g[i][j]] d[g[i][j]]相同, g [ i ] [ j ] g[i][j] g[i][j]才可以合并。

获得 g [ i ] [ j ] g[i][j] g[i][j]后,我们通过现有信息判断到原序列第 i i i个数为止,能够获得的最短序列是多少,设其为 d p [ i ] dp[i] dp[i]。首先对于 d [ i ] = 0 d[i]=0 d[i]=0的位置,我们直接可知 d p [ i + 1 ] = d p [ i ] dp[i+1]=dp[i] dp[i+1]=dp[i],因为直接合并掉了。如果 d [ i ] ≠ 0 d[i]\neq 0 d[i]=0,那么看能够形成 d [ i ] × 2 j d[i]\times 2^j d[i]×2j i i i中,最小的 d p [ g [ i ] [ j ] ] dp[g[i][j]] dp[g[i][j]],并令 d p [ i ] = min ⁡ ( d p [ i ] , d p [ g [ i ] [ j ] ] + 1 ) dp[i]=\min(dp[i],dp[g[i][j]]+1) dp[i]=min(dp[i],dp[g[i][j]]+1)

最后结果是 d p [ n ] dp[n] dp[n]

#include
using namespace std;
typedef long long LL;
const int N = 300005;
const LL INF = 1e18 + 1;

LL n;
LL a[N], d[N], g[N][65], cnt[N], dp[N];

void main2() {
	cin >> n;
	d[0] = 0;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		cnt[i] = d[i] = 0;
		for (int j = 0; j < 64; ++j) {
			g[i][j] = -1;
		}
		dp[i] = INF;
	}
	for (int i = 1; i < n; ++i) {
		d[i] = a[i + 1] - a[i];
	}
	for (int i = 1; i < n; ++i) {
		if (d[i] == 0) d[i] = INF;
		else {
			while (d[i] % 2 == 0) {
				++cnt[i]; d[i] >>= 1;
			}
		}
	}
	for (int i = n - 1; i >= 1; --i) {
		g[i][cnt[i]] = i + 1;
		for (int j = cnt[i] + 1; j <= 63; ++j) {
			if (g[i][j - 1] == -1) break; 
			if (g[i][j - 1] >= n) break;
			if (d[g[i][j - 1]] != d[i]) break;
			int k = g[i][j - 1];
			g[i][j] = g[k][j - 1];
		}
	}
	dp[1] = 1;
	for (int i = 1; i < n; ++i) {
		if (d[i] == INF and d[i + 1] == INF) {
			dp[i + 1] = min(dp[i + 1], dp[i]);
		}
		else {
			for (int j = 0; j < 64; ++j) {
				if (g[i][j] != -1) {
					dp[g[i][j]] = min(dp[g[i][j]], dp[i] + 1);
				}
			} 
		}
	}
	cout << dp[n] << '\n';
}

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

604 - 环的数量

题目链接

点数很少,可以考虑使用状态转移。用一个包含 n n n位的二进制数表示当前要判断的点的集合,如果第 i i i位为 1 1 1,则意味着集合中包含第 i i i个点(这里从 0 0 0开始编号)。接下来判断每一个集合能否形成一个环。如果这个集合能够形成一个环,那么一定选择任意一个点作为起点,都可以走过一条路径,不重复经过其他点地走回这个起点。所以为了方便起见,我们选择集合中编号最小的点作为起点。对于一个表示状态的二进制数 S S S,起点的编号就是 l o w b i t ( S ) lowbit(S) lowbit(S)

d p [ S ] [ i ] dp[S][i] dp[S][i]表示状态为 S S S的集合中走到编号为 i i i的点的方案数。初始条件下,只有一个点的集合在这个点的位置的方案数是 1 1 1(用于初始化)。

接下来对于每一个状态,我们看能否从当前的状态向其他的状态去转移,比如说从状态 S S S上的点 u u u跑到一个不在 S S S集合内的一个新点 v v v上,那么从这里转移过去的那个状态就是 S ∣ ( 1 < < u ) S|(1<S(1<<u),那个状态的方案数要加上这个状态 u u u点处的方案数。

如果遍历结点的时候遍历到了这个集合的起点,那就将那个转移量加入到答案当中。

#include
using namespace std;
typedef long long LL;

int n, m;
LL ans = 0;

int lowbit(int x) {
	return x & (-x);
}

LL e[22][22], dp[(1 << 22) + 5][22];

void main2() {
	cin >> n >> m;
	for (int i = 0; i <= n; ++i) {
		for (int j = 0; j <= n; ++j) {
			e[i][j] = 0;
		}
	}
	for (int i = 1; i <= m; ++i) {
		int x, y;
		cin >> x >> y;
		--x; --y;
		e[x][y] = e[y][x] = 1;
	}
	for (int i = 0; i < n; ++i) {
		dp[(1 << i)][i] = 1;
	}
	for (int S = 1; S < (1 << n); ++S) {
		int s = log2(lowbit(S));
		for (int i = 0; i < n; ++i) {
			if ((S & (1 << i)) == 0) continue;
			if (e[s][i]) ans += dp[S][i];
			for (int j = s + 1; j < n; ++j) {
				if (!e[i][j]) continue;
				if ((S & (1 << j)) > 0) continue;
				dp[(S | (1 << j))][j] += dp[S][i];
			}
		}
	}
	cout << (ans - m) / 2;
}

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

605 - Ayoub’s function

题目链接

我们发现,整个01串可以写成 a 0 a_0 a0 0 0 0 1 1 1 1 1 1 a 1 a_1 a1 0 0 0 1 1 1 1 1 1 a 2 a_2 a2 0 0 0 1 1 1 1 1 1 a 3 a_3 a3 0 0 0……

因为 n n n的数据范围比较大,所以如果从 1 1 1开始去数会有点困难,我们可以反向思考,我们只需要数出所有不包含 1 1 1的区间个数,然后用总的区间个数去减一下就好了。

我们发现,答案就是 n ( n − 1 ) 2 − ∑ i = 0 m a i ( a i − 1 ) 2 \frac{n(n-1)}{2}-\displaystyle\sum\limits_{i=0}^{m} \frac{a_i(a_i-1)}{2} 2n(n1)i=0m2ai(ai1)

当每个 a i a_i ai相等或近似相等时,答案就是最大的。所谓近似相等,就是如果 0 0 0的个数 n − m n-m nm与其区间个数 m + 1 m+1 m+1如果不是整除关系,那么会造成 a i a_i ai之间不同,而多出来的模数,让他们平均地分摊到每一个 a i a_i ai中。于是 1 1 1 1 1 1之间的 0 0 0的个数要么是 x x x,要么是 x + 1 x+1 x+1,其中 x = ⌊ n − m m + 1 ⌋ x=\lfloor \frac{n-m}{m+1} \rfloor x=m+1nm

#include
using namespace std;
typedef long long LL;

LL n, m, p, tot; 

void main2() {
	cin >> n >> m;
	tot = (n + 1) * n / 2;
	p = (n - m) / (m + 1);
	if ((n - m) % (m + 1) == 0) {
		cout << tot - (m + 1) * p * (p + 1) / 2 << '\n';
	}
	else {
		LL r = (n - m) % (m + 1);
		cout << tot - r * (p + 1) * (p + 2) / 2 - (m + 1 - r) * p * (p + 1) / 2 << '\n';
	}
}

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

606 - 最大权值划分

题目链接

需要注意到一个非常重要的性质:如果我们想考察如何划分,就要尽量让每一个数在各自被划出来的区间里起到贡献,也就是说,我们争取划分的区间要么是单调递增的,要么是单调递减的。

可以明确的是:只有一个元素的区间是完全无效的,因为它为答案带来的贡献是 0 0 0

上面的性质可以这样理解:假设有 3 3 3个数: 4 , 2 , 7 4,2,7 4,2,7,那么贡献产生自 2 , 7 2,7 2,7。如果 4 4 4被塞入到这个区间,那么与不塞进来的贡献是一样的,因为最值取自于 2 2 2 7 7 7,与 4 4 4无关。他只有在向其他区间合并的时候,才可能产生一个贡献。

对于一个单调的区间,虽然最值只取于两边的数,但是想选择两边的数,中间的数也不得不取。试想从中间切开,会发现答案变小了。

带着这样的性质,我们可以考虑 D P DP DP:对于每一个数,如果这个数位于单调递增区间当中,那么从第一个数到这个数的答案是 d p [ i ] [ 0 ] dp[i][0] dp[i][0];如果这个数位于单调递减区间当中,那么从第一个数到这个数的答案是 d p [ i ] [ 1 ] dp[i][1] dp[i][1]

分两种情况讨论:

  1. a i > a i − 1 a_i>a_{i-1} ai>ai1,如果自己在单调递增区间当中,那么 a i a_i ai可以继承上一个数的结果,也可以作为单调递增的区间的第一个元素(上一个元素被分到了单调递减的区间),所以有 d p [ i ] [ 0 ] = max ⁡ { d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + a i − a i − 1 } dp[i][0]=\max\{ dp[i-1][1],dp[i-1][0]+a_i-a_{i-1} \} dp[i][0]=max{dp[i1][1],dp[i1][0]+aiai1}。如果自己在单调递减的区间当中,那么自己一定是在递减区间的第一个元素,不知道上一个元素被分到的是递增区间还是递减区间,所以取其中的最大值,即 d p [ i ] [ 1 ] = max ⁡ { d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] } dp[i][1]=\max\{dp[i-1][0],dp[i-1][1]\} dp[i][1]=max{dp[i1][0],dp[i1][1]}
  2. a i ≤ a i − 1 a_i\leq a_{i-1} aiai1,如果自己在单调递减区间当中,那么 a i a_i ai可以继承上一个数的结果,也可以作为单调递增的区间的第一个元素(上一个元素被分到了单调递增的区间),所以有 d p [ i ] [ 1 ] = max ⁡ { d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + a i − 1 − a i } dp[i][1]=\max\{ dp[i-1][0],dp[i-1][1]+a_{i-1}-a_{i} \} dp[i][1]=max{dp[i1][0],dp[i1][1]+ai1ai}。如果自己在单调递增的区间当中,那么自己一定是在递增区间的第一个元素,不知道上一个元素被分到的是递增区间还是递减区间,所以取其中的最大值,即 d p [ i ] [ 0 ] = max ⁡ { d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] } dp[i][0]=\max\{dp[i-1][0],dp[i-1][1]\} dp[i][0]=max{dp[i1][0],dp[i1][1]}
#include
using namespace std;
typedef long long LL;

LL n;
LL a[1000005], dp[1000005][2];

void main2() {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		dp[i][0] = dp[i][1] = -1e18;
	}
	dp[1][0] = dp[1][1] = 0;
	for (int i = 2; i <= n; ++i) {
		if (a[i] > a[i - 1]) {
			dp[i][1] = max(dp[i - 1][0], dp[i - 1][1]);
			dp[i][0] = max(dp[i - 1][1], dp[i - 1][0] + a[i] - a[i - 1]);
		}
		else {
			dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
			dp[i][1] = max(dp[i - 1][0], dp[i - 1][1] + a[i - 1] - a[i]);
		}
	}
	cout << max(dp[n][0], dp[n][1]);
}

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

607 - 括号序列

题目链接

从最简单的括号序列看起。假设我们现在要处理的括号序列是()()()()。

我们发现有 4 4 4个独立的括号序列,所以答案是 4 × ( 4 + 1 ) 2 = 10 \frac{4\times (4+1)}{2}=10 24×(4+1)=10

现在让情况稍微复杂一点,考虑这样的括号序列:()()()(())。(在最后一个独立的括号序列里嵌套了一个独立序列)

我们发现,这种情况下,外层的 4 4 4个独立的括号序列对答案的贡献不变,依旧是 4 × ( 4 + 1 ) 2 = 10 \frac{4\times (4+1)}{2}=10 24×(4+1)=10,多出来的贡献只有第 4 4 4个独立括号中多出的合法括号序列()。这时这个多出来的独立括号序列对答案的贡献就是 1 1 1。因此此时的答案是 11 11 11

再复杂一点?如果我们面对的括号序列是()()()(()()),将第四个独立括号序列中变成两个并列的合法括号序列。那么答案就是 10 + 3 = 13 10+3=13 10+3=13,最外层的 4 4 4个独立括号序列对答案的贡献是 10 10 10,第 4 4 4个括号里面对答案的贡献是 2 × ( 2 + 1 ) 2 = 3 \frac{2\times (2+1)}{2}=3 22×(2+1)=3

好像有点头绪了。我们只需要统计出每一层的独立括号数量 x x x,这一层的独立括号对答案的贡献就是 x × ( x + 1 ) 2 \frac{x\times(x+1)}{2} 2x×(x+1)。如果这一层的独立括号内部仍然有独立括号,就先统计里面的贡献,然后单独算外面的贡献。

可以用dfs实现,但是有一个问题,就是题目并没有保证给定的括号序列是合法的。

为了简化实现过程,我们可以考虑用栈。我们每遇到一个左括号,就将一个左括号推入栈中,并初始化以这个左括号为一对左括号的左侧的一对括号内部,有多少个匹配的括号序列对答案产生贡献。每遇到一个右括号,需要分情况讨论,如果此时栈为空,那么这个右括号是不合法的,此时整体的答案贡献在计算到这里后应当截止于此,因为有这样一个不匹配的右括号拦在这里,这个右括号左右侧是没有办法一起计算的,所以干脆直接将这个括号的左边的贡献算进答案里,然后将这个括号及其左侧的所有括号全部删除(即从下一个括号开始从头考虑)。

为了实现这样一个过程,我们规定下标 0 0 0为虚空的左括号(读入字符串时需向右平移一个下标读入),这个虚空左括号用来统计最外层的括号数量。然后对于每一个括号进行如下操作:

  1. 如果遇到的是一个左括号,那么像栈中推入一个值为 0 0 0的元素,表示一个新的待匹配括号。
  2. 如果遇到的是一个右括号,而且此时栈不为空,那么将栈顶的左括号的值(以这个左括号为左半边的括号对内部的独立的匹配括号组数)的贡献算进答案,然后用这个右括号将其匹配掉(将其从栈中弹出),并让弹出后的栈顶的值 + 1 +1 +1,表示以弹出后的栈顶的左括号为左半边的括号对的内部新增了一个括号序列。
  3. 如果遇到的是一个右括号,而且此时栈为空,那么将当前下标为 0 0 0的位置的值计算贡献,算进答案(当前这个右括号之前合法的独立括号组数产生的答案的贡献)。
#include
using namespace std;
typedef long long LL;

string str;
int a[1000005], s[1000005];
LL ans = 0, n;

void calc(LL x) {
	ans += (x * (x + 1) / 2);
}

void main2() {
	cin >> str;
	int ai = 0, si = 0;
	n = str.length();
	for (char c: str) {
		if (c == '(') a[++ai] = 1;
		else a[++ai] = -1;
	}
	for (int i = 1; i <= n; ++i) {
		if (si == 0 and a[i] == -1) {
			calc(s[0]);
			s[0] = 0;
			continue;
		}
		if (a[i] == 1) {
			s[++si] = 0;
		}
		else {
			calc(s[si--]);
			++s[si];
		}
	}
	while (si >= 0) {
		calc(s[si--]);
	}
	cout << ans;
}

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

你可能感兴趣的:(算法学习,做题笔记,算法,图论,c++)