「Pudding Monsters」Solution

简述题意

给定一个 n × n n \times n n×n 的棋盘,其中有 n n n 个棋子,每行每列恰好有一个棋子。

对于所有的 1 ≤ k ≤ n 1 \leq k \leq n 1kn,求有多少个 k × k k \times k k×k 的子棋盘中恰好有 k k k 个棋子,输出其总和。

  • n ≤ 3 × 1 0 5 n \le 3 \times 10^5 n3×105

思路

在某个 k × k k \times k k×k 的子矩阵中,显然行是连续的,列也是连续的。并且,每行每列恰好只有一个棋子,即每一行都唯一对应着某一列,这都启发我们把二维平面压缩成一个排列,即令 a [ x ] = y a[x]=y a[x]=y
那么 k × k k \times k k×k 的子矩阵中有 k k k 1 1 1 就等价于 a a a 中某段子序列的值是连续的,原题就转化为经典的 值域连续段个数 问题。

可能有点抽象,举个例子:
a : [ 1 , 5 , 2 , 4 , 3 ] a:[1,5,2,4,3] a:[1,5,2,4,3],满足条件的子序列就有 [ 1 ] , [ 5 ] , [ 2 ] , [ 4 ] , [ 3 ] , [ 4 , 3 ] , [ 2 , 4 , 3 ] , [ 5 , 2 , 4 , 3 ] [ 1 , 5 , 2 , 4 , 3 ] [1],[5],[2],[4],[3],[4,3],[2,4,3],[5,2,4,3][1,5,2,4,3] [1],[5],[2],[4],[3],[4,3],[2,4,3],[5,2,4,3][1,5,2,4,3],如果还觉得抽象可以把上述子序列还原成二维,可以加深理解。

然而,值域连续是很难刻画的,一般从最值入手,即如果某个子序列 [ l , r ] [l,r] [l,r] 值域连续,其一定满足:

max ⁡ { a l , a l + 1 , . . . , a r − 1 , a r } − min ⁡ { a l , a l + 1 , . . . , a r − 1 , a r } = r − l \max\{a_l,a_{l+1},...,a_{r-1},a_r\} - \min\{a_l,a_{l+1},...,a_{r-1},a_r\}=r-l max{al,al+1,...,ar1,ar}min{al,al+1,...,ar1,ar}=rl

还有一个条件是 [ l , r ] [l,r] [l,r] 中每一个数仅出现一次,然而这个条件在此题中是天然满足的。

因此,我们只需要知道有哪些子序列的最大值减去最小值等于序列长度即可,对于序列计数问题,一般都是固定某个端点进行统计,而值域连续段就是通过固定右端点实现的。

不妨令现在的右端点为 r p o s rpos rpos,并用一颗线段树维护左端点,具体地,某个节点 [ l , r ] [l,r] [l,r] 表示的就是对于 ∀ i ∈ [ l , r ] , [ i , r p o s ] \forall i \in[l,r],[i,rpos] i[l,r][i,rpos] m a x − m i n + i − r p o s \mathrm{max} - \mathrm{min} + i - rpos maxmin+irpos 的值,然而,我们是难以统计上述式子的所有取值的,因此引入一个性质:

  • 对于某个子序列 [ l , r ] [l,r] [l,r],一定有 m a x − m i n + l − r ≥ 0 \mathrm{max} - \mathrm{min} + l - r \ge 0 maxmin+lr0

因此,对于当前右端点,由于 [ r p o s , r p o s ] [rpos,rpos] [rpos,rpos] 上述式子的值显然为 0 0 0,所以 r p o s rpos rpos 结尾的所有子序列中上述式子的最小值一定是 0 0 0。因此,我们只需要维护线段树上的最小值以及最小值出现的次数,就是 m a x − m i n + l − r = 0 \mathrm{max} - \mathrm{min} + l - r = 0 maxmin+lr=0 的个数。

考虑如何维护线段树。

  • 对于式子中的 + l +l +l,直接将线段树叶子节点(即左端点)的初值置为 l l l 即可。
  • 对于式子中的 − r -r r,我们每次 r ← r + 1 r \leftarrow r+1 rr+1 时,给全局每一个位置减去 1 1 1 即可。
  • 而子序列最大值,最小值就用单调栈维护。以最大值为例,记单调栈为 s s s,栈顶为 t o p top top,如果 a s t o p ≤ a i a_{s_{top}} \le a_i astopai,那么就说明,对于 j ∈ [ s t o p − 1 + 1 , s t o p ] , [ j , i ] j \in [s_{top-1}+1,s_{top}],[j,i] j[stop1+1,stop],[j,i] 这些子序列,其的最大值由 a s t o p a_{s_{top}} astop 变为了 a i a_i ai,那么我们给区间 [ s t o p − 1 + 1 , s t o p ] [s_{top-1}+1,s_{top}] [stop1+1,stop] 加上一个 a i − a s t o p a_i-a_{s_{top}} aiastop 即可,然后 t o p ← t o p − 1 top \leftarrow top-1 toptop1 继续维护单调栈即可。最小值同理,不过区间加变为区间减了。

代码

上述做法就是值域连续段的经典解法,算是一个 trick \text{trick} trick 吧 (((

#include<bits/stdc++.h>
#define int long long
#define pii pair<int , int>
using namespace std;
const int MAXN = 3e5 + 5;
namespace Segment{
	struct tree{
		int l , r , add , Min , cnt;
	}tree[MAXN << 3];
	void pushup(int p) {
		tree[p].Min = min(tree[p << 1].Min , tree[p << 1 | 1].Min) , tree[p].cnt = 0;
		if (tree[p << 1].Min == tree[p].Min) tree[p].cnt += tree[p << 1].cnt;
		if (tree[p << 1 | 1].Min == tree[p].Min) tree[p].cnt += tree[p << 1 | 1].cnt;
	}
	void pushdown(int p) {
		tree[p << 1].Min += tree[p].add , tree[p << 1 | 1].Min += tree[p].add;
		tree[p << 1].add += tree[p].add , tree[p << 1 | 1].add += tree[p].add;
		tree[p].add = 0;
	}
	void build(int p , int l , int r) {
		tree[p].l = l , tree[p].r = r , tree[p].add = 0;
		if (l == r){
			tree[p].Min = l , tree[p].cnt = 1;
			return;
		}
		int mid = l + r >> 1;
		build(p << 1 , l , mid) , build(p << 1 | 1 , mid + 1 , r);
		pushup(p);
	}
	void update(int p , int l , int r , int v) {
		if (tree[p].l >= l && tree[p].r <= r) {
			tree[p].add += v , tree[p].Min += v;
			return;
		}
		pushdown(p);
		int mid = tree[p].l + tree[p].r >> 1;
		if (l <= mid) update(p << 1 , l , r , v);
		if (r > mid) update(p << 1 | 1 , l , r , v);
		pushup(p);
	}
	pii query(int p , int l , int r) {
		if (tree[p].l >= l && tree[p].r <= r) return make_pair(tree[p].Min , tree[p].cnt);
		pushdown(p);
		int mid = tree[p].l + tree[p].r >> 1;
		pii tmp1 , tmp2;
		tmp1.first = tmp2.first = 1e9;
		if (l <= mid) tmp1 = query(p << 1 , l , r);
		if (r > mid) tmp2 = query(p << 1 | 1 , l , r);
		if (tmp1.first < tmp2.first) return tmp1;
		if (tmp1.first > tmp2.first) return tmp2;
		return make_pair(tmp1.first , tmp1.second + tmp2.second);
	}
}
using namespace Segment;
int n , a[MAXN] , s1[MAXN] , top1 , s2[MAXN] , top2 , ans;
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr) , cout.tie(nullptr);
	cin >> n;
	for (int i = 1 , x , y ; i <= n ; i ++) {
		cin >> x >> y;
		a[x] = y;
	}
	build(1 , 1 , n);
	for (int i = 1 ; i <= n ; i ++) {
		update(1 , 1 , n , -1);
		while (top1 && a[s1[top1]] <= a[i]) update(1 , s1[top1 - 1] + 1 , s1[top1] , a[i] - a[s1[top1]]) , top1 --;
		s1[++ top1] = i;
		while (top2 && a[s2[top2]] >= a[i]) update(1 , s2[top2 - 1] + 1 , s2[top2] , a[s2[top2]] - a[i]) , top2 --;
		s2[++ top2] = i;
		ans += query(1 , 1 , i).second;
	}
	cout << ans;
	return 0;
}
  • 33
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我可以为您讲解一下如何使用Java中的装饰者模式来设计实现奶茶订单问题。 首先,我们可以定义一个基础的奶茶类,包含一些最基本的属性和方法,例如: ```java public abstract class MilkTea { String description = "Unknown Milk Tea"; public String getDescription() { return description; } public abstract double cost(); } ``` 接下来,我们可以定义一些具体的奶茶类,例如: ```java public class BlackMilkTea extends MilkTea { public BlackMilkTea() { description = "Black Milk Tea"; } public double cost() { return 10.0; } } public class GreenMilkTea extends MilkTea { public GreenMilkTea() { description = "Green Milk Tea"; } public double cost() { return 12.0; } } ``` 然后,我们可以定义一个装饰者类,用于对奶茶进行装饰,例如: ```java public abstract class MilkTeaDecorator extends MilkTea { public abstract String getDescription(); } ``` 接下来,我们可以定义一些具体的装饰类,例如: ```java public class PearlDecorator extends MilkTeaDecorator { private MilkTea milkTea; public PearlDecorator(MilkTea milkTea) { this.milkTea = milkTea; } public String getDescription() { return milkTea.getDescription() + ", add pearl"; } public double cost() { return milkTea.cost() + 2.0; } } public class PuddingDecorator extends MilkTeaDecorator { private MilkTea milkTea; public PuddingDecorator(MilkTea milkTea) { this.milkTea = milkTea; } public String getDescription() { return milkTea.getDescription() + ", add pudding"; } public double cost() { return milkTea.cost() + 3.0; } } ``` 最后,我们可以在客户端代码中进行测试,例如: ```java public class Client { public static void main(String[] args) { MilkTea milkTea = new BlackMilkTea(); milkTea = new PearlDecorator(milkTea); milkTea = new PuddingDecorator(milkTea); System.out.println(milkTea.getDescription() + " " + milkTea.cost()); } } ``` 以上就是使用Java中的装饰者模式来设计实现奶茶订单问题的示例代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值