清北学堂 2020 国庆J2考前综合强化 Day2

1. 题目

T1 一

题目描述

问题描述

你是能看到第一题的 friends 呢。
——hja

众所周知,小葱同学擅长计算,尤其擅长计算组合数,但这个题和组合数没什么关系。

现在某公司有若干底层人员处理了若干订单,每个底层人员有一个中层管理作为他的上司,而每个中层管理也有一个高层管理作为他的上司。现在要进行年终评审,我们需要按业绩对所有公司人员进行排序,每个人的业绩为他所有下述的订单数量之和,输出这个排序结果。

输入格式

输入首先包含若干行字符串,每行字符串的格式为 姓名,订单数,代表一个底层人员的信息。

接下来一行空行。

接下来包含若干行字符串,每行字符串的格式为 高层管理,中层管理,底层人员,代表一组从属关系。

最后一行 eof,代表输入结束。

输出格式

输出按照业绩作为关键字进行排序,如果业绩相同则按照名字字典序进行排序。如果当前输出的是一名高层管理,则接下来先对其下属的中层管理进行排序输出:

  • 如果当前输出的是一名高层管理,则接下来先对其下属的中层管理进行排序输出;
  • 如果当前输出的是一名中层管理,则接下来先对其下属的底层人员进行排序输出。

在每个人进行输出的时候,首先输出其所属层级,如果为高层管理则不用输出任何内容,如果为中层人员需要先输出 -,如果为底层人员需要先输出 --。接下来输出该人员的姓名,再输出 <业绩>

样例输入

A,125
B,110
C,92
D,154

E,F,A
E,F,B
E,G,C
E,G,D
eof

样例输出

E<481>
-G<246>
--D<154>
--C<92>
-F<235>
--A<125>
--B<110>

数据规模与约定

对于 \(100\%\) 的数据,所有人的名字长度不超过 \(50\) 且不包含空格、逗号,所有订单数量大小不超过 \(1000\),总共的底层人员数量不超过 \(1000\),可能出现某人的名字为 eof

Sol

恶心,读入时直接统计 , 的数量即可,不用判空行。

然后用 sscanf 就可以读信息了。

T2 二

题目描述

问题描述

你是能看到第二题的 friends 呢。
——aoao

众所周知,小葱同学擅长计算,尤其擅长计算组合数,但这个题和组合数没什么关系。

小 A 从位置 \(0\) 出发,每秒钟速度为 \(a\)。小 B 从位置 \(x\) 出发,每秒钟速度为 \(b(a>b)\)。小 A 每次追上小 B 之后,便掉头开始返回位置 \(0\),抵达后再反过来追小 B,问第 \(k\) 次追上小 B 的时间为多少。

输入格式

第一行四个整数,\(a,b,x,k\)

输出格式

一行一个整数代表追上的时间对 \(10^9+7\) 取模后的结果,注意由于答案可能有小数存在,所以这里小数需要在逆元意义下取模。即除以 \(k\) 等价于乘以 \(k^{10^9+5}\)

样例输入

2 1 1 10

样例输出

39365

数据规模与约定

  • 对于 \(30\%\) 的数据,答案在取模之前为整数。
  • 对于 \(80\%\) 的数据,\(k\le 10^3\)
  • 对于 \(100\%\) 的数据,\(1≤b<a≤100\)\(1≤x,k≤10^9\)

Sol

显然第一轮就是个追及问题,时间就是 \(\dfrac{x}{a-b}\)

我们关系后几轮,设 A 已经追上了 B,相遇点为 \(x\),现在 A 往回走,B 往前走。

则 A 再次追上 B 的时间为

\[\begin{aligned}t&=\dfrac xa+x\cdot\dfrac{a+b}a+\dfrac{1}{a-b}\\&=\dfrac xa\cdot\left(1+\dfrac{a+b}{a-b}\right)\\&=\dfrac xa\cdot\dfrac{2a}{a-b}\\&=\dfrac{2x}{a-b}\end{aligned} \]

此时,B 走到了

\[\begin{aligned}x'&=x+tb\\&=x+\dfrac{2b}{a-b}\cdot x\\&=x\cdot\left(1+\dfrac{2b}{a-b}\right)\\&=x\cdot\dfrac{a+b}{a-b}\end{aligned} \]

故每次走一次路程增加 \(\dfrac{a+b}{a-b}\) 倍,显然,时间每次也增加 \(\dfrac{a+b}{a-b}\) 倍。

所以说总时间就是个等比数列求和,直接套公式即可。

T3 三

题目描述

问题描述

你是能看到第三题的 friends 呢。
——laekov

众所周知,小葱同学擅长计算,尤其擅长计算组合数,但这个题和组合数没什么关系。

给定字符串 \(s\),求有多少种方法将 \(s\) 分割为若干伪回文字符串。所谓伪回文字符串指的是删除至多一个字符之后能够变成回文串的字符串。

输入格式

一行一个字符串 \(s\)

输出格式

输出一行一个整数代表答案对 \(10^9+7\) 取模之后的结果。

样例输入

aaa

样例输出

4

数据规模与约定

  • 对于30%30%的数据,字符串长度不超过 \(10\) .
  • 对于60%60%的数据,字符串长度不超过 \(100\) .
  • 对于100%100%的数据,字符串长度不超过 \(2000\) .

Sol

先处理 \(l\sim r\) 是否为伪回文串。

\(is_{l,r}\) 表示 \(l\sim r\) 是否为回文串。

如果暴力求显然是 \(O(n^3)\) 的。

注意到 \(is\) 有如下性质:

  • \(is_{i,i}=\bf true\)
  • \(is_{l,r}=is_{l+1,r-1}\land[a_l=a_r]\)(其中 \(a\) 为原字符串),也就是中间是回文串且两段相同。

这样处理就只需要 \(O(n^2)\) 的复杂度了。

那怎么可以将其变为伪回文串的判断呢?

上面这个东西很像 dp,从 dp 角度考虑,至多删一个字符,所以要多一个维度。

\(is_{l,r,s}\) 表示 \(l\sim r\) 要删掉几个字符可以变成回文串(\(s\in\{0,1\}\)

转移:

  • \(is_{l,r,0}\) 同标准回文串:\(is_{l,r,0}=is_{l+1,r-1,0}\land[a_l=a_r]\) .
  • \(is_{l,r,1}\) 的求法是讨论删除的这个字符串在哪里:
    • 删除 \(l\) 处的字符串:\(is_{l+1,r,0}\)
    • 删除 \(l,r\) 中间的字符串:\(is_{l+1,r-1,0}\land[a_l=a_r]\)
    • 删除 \(r\) 处的字符串:\(is_{l,r-1,0}\) .

这三个取个 or 即可。

再维护一个 \(able_{l,r}=is_{l,r,0} \lor is_{l,r,1}\) 表示 \(l\sim r\) 是不是一个伪回文串。

接下来回到原题,考虑 dp。

\(dp_i\) 表示 \(1\sim i\) 位拆分成若干个伪回文串的方案数。

考虑如果在第 \(j\) 位可以拆分(即 \(able_{j+1,i}=\bf true\),后面为一伪回文串),那么 \(dp_i\) 就加上 \(dp_j\) 的方案数。

for (int i=1;i<=n;i++)
	for (int j=0;j<i;j++)
		dp[i]+=dp[j]*able[j+1][i];  // <=> if (able[j+1][i]) dp[i]+=dp[j];

T4 四

题目描述

问题描述

你是能看到第三题的 friends 呢。
——laekov

众所周知,小葱同学擅长计算,尤其擅长计算组合数,但这个题和组合数没什么关系。

现在我们有一张 \(N\times N\) 的地图上 ABCDEF 六种方块,其中 ABCD 为普通方块,E 方块可以当做 ABC 中的任意一种,而 F 方块被消除时会把周围八个方块全部消除。当出现至少三个相同的方块在一行或者一列时这些方块就会被消除。当一系列消除完成之后,所有方块会进行下落操作,如果产生新的可消除方块便会继续进行消除直到没有能够被消除的方块。现在你可以执行一次交换相邻两个方块的操作,问最多能够消除多少个方块。

输入格式

第一行一个整数 \(N\)

接下来 \(N\) 行每行 \(N\) 个字母。

输出格式

一行一个整数代表答案。

样例输入

4
AABA
BBEB
CDCD
FFFF

样例输出

16

数据范围与规定

本题一共 \(10\) 组数据点,对于第 \(i\) 组数据,\(N=3i\)。注意输入的最后一行是最下方,并且一开始可能有三个一样的连在一起,但只有进行一次交换操作后才会开始消除。

Sol

暴力模拟。

2. 算法 -- 数据结构

数组:\(\rm[data,data,data,\cdots]\)

链表:\(\rm data\to data\to data\to \cdots\)

队列:先进先出(First In First Out,FIFO)
栈:先进后出(First In Last Out,FILO)

堆:操作:

  • 加一个数 x
  • 删掉最大/小的数
  • 询问最大/小的数

存二叉树:一维数组,二叉树编号,若目前节点编号为 \(i\),则其左儿子编号为 \(2i\),右儿子编号为 \(2i+1\),父亲编号为 \(\left\lfloor\dfrac i2\right\rfloor\)

大根堆:任意节点都比其儿子节点大(故求最大值直接返回根)

插入操作:
若插入 \(v\),那么先直接把 \(v\) 丢到最后一个节点的后一位。
为了满足堆的性质,故若其比它的父节点大,那么将其交换,若还不满足,那么继续交换 \(\cdots\)

删除操作:
显然根是最大的,所以要把根删掉,为了满足它还是个二叉树,所以将其与最后一个节点交换,然后将根删除。
换上来那个数可能不满足堆的性质,故若其比它的至少一个儿子大,那么选择最大的(为了让其交换后不会再次违背堆的性质)那个儿子根其交换,若还不满足,那么继续交换 \(\cdots\)

struct heap // 整数类型大根堆 
{
	static const N=23333;
	int n,z[N]; // z 是这棵树
	heap(){n=0;} // 初始化 
	int top(){return z[1];} // 查询操作 
	inline void insert(int x) // 插入操作 
	{
		z[++n]=x; int p=n;
		while ((p>1)&&(z[p]>z[p/2])) swap(z[p],z[p/2]),p/=2; // 交换并且将 p 上移 
	}
	inline void remove() // 删除操作 
	{
		swap(z[1],z[n]); --n; int p=1;
		while (2*p<=n) // 有儿子 
		{
			int pp=2*p;
			if ((pp+1<=n)&&(z[pp+1]>z[pp])) ++pp; // 右儿子存在且比左儿子大,那么使 pp 变成右儿子,此时 pp 是左右最大的儿子。
			if (z[p]<z[pp]){swap(z[p],z[pp]); p=pp;} // 下移 
		}
	}
};

Problem 1:给定 \(n\) 个数,求平均值最大的子区间

显然最大值和一个更小的值取平均数会变小,所以直接输出序列中的最大值即可。


Problem 2:给定 \(n\) 个数,求 \(\min(a_i,a_{i+1},\cdots,a_j)\cdot|i-j|\) 的最大值

如果选定区间中包含最小值,那么 \(\min(a_i,a_{i+1},\cdots,a_j)\) 就固定了,为了最大化 \(|i-j|\),直接选整个序列即可。

如果这个区间中不包含最小值,显然 \(l\)\(r\) 都在最小值左边或者都在最小值右边。

那么两边的问题就和整个问题一模一样了,递归求解即可。
这个最小值用 st 表维护一下即可。

st 表:

\(f_{i,j}\) 为从 \(i\) 开始 \(2^j\) 个数的最小值。
那么显然 \(f_{i,0}=a_i\)

\(f_{i,j}\):考虑将序列分成两半,故转移为 \(f_{i,j}=\min(f_{i,j-1},f_{i+2^{j-1},j-1})\) .
注意因为 \(f_{i,j}\) 是根据 \(f_{\dots\,,\,j-1}\) 转移的,所以要先转移 \(j\)

for (int i=1;i<=n;i++) f[i][0]=a[i]; // 初始化 
for (int j=1;(1<<j)<=n;j++)
	for (int i=1;i+(1<<j)-1<=n;i++)
		f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]); // 转移

考虑区间最小值如何维护。

若询问 \(\min(a_1,a_2,a_3,a_4)\),那么显然它就是 \(f_{1,2}\) .
若询问 \(\min(a_1,a_2,a_3,a_4,a_5)\),注意到 \(\min\) 中多次出现数不会影响答案,故可以用两个 \(4\) 盖住 \(5\),答案为 \(\min(f_{1,2},f_{2,2})\) .
若询问 \(\min(a_1,\dots,a_{15})\),同理,用两个 8 盖住 15 即可。

做法:

/* 预处理 */
for (int i=0;(1<<i)<=n;i++)
	for (int j=(1<<i);j<(1<<(i+1));j++) use[i]=j; // 询问时就可以用两个 use[i] 盖住 i 了 
/* 询问时 */
int l,r;
...
int len=r-l+1,j=use[len];
cout<<min(f[l][j],f[r-(1<<j)+1][j]); // 最小值 

Problem 3:给定 \(n\) 个数,\(n\) 组询问,每次求区间中位数。

先写个暴力,考虑优化找中位数部分。

把这个序列分成大小两半,大的扔到大根堆,小的扔到小根堆
此时,如果 \(n\) 是偶数,那么中位数就是两个堆顶的平均数;否则中位数就是两个堆顶较大的数。
考虑加入一个数,先看看它要放在左边还是右边,如果两个堆的大小之差超过 \(1\) 了(不是中位数了),就把那个多的堆的堆顶丢到另一个堆里。
复杂度 $O(n^2\log n) $

更优的做法:

把操作反过来,插入变成删除
\(a_l \cdots a_n\) 从小到大链成链表
先维护一个中位数指针,然后删除就相当于把指针移动 \(0.5\) 格,这样维护即可。
容易发现删除和移动都是 \(O(1)\) 的,所以总复杂度为 \(O(n^2)\)


Problem 4:给定一棵 \(n\) 个点的树,每个点的点权是 \(a_i\),有 \(m\) 组询问,每次询问树中 \(p_1\)\(p_2\) 上的简单路径中是否能找到三个点的点权使得其能作为一个三角形的三边。
\(1\le N,M\le 10^5,1\le a_i\le 2^{31}-1\) .

考虑一个序列使得任意三条边都不能组成三角形。
先让前两条边为 \(1\)
第三个数容易发现最小为 \(1+1=2\)
第四个数,最小为 \(1+2=3\)
第五个数,最小为 \(2+3=5\)
...

显然这是个 Fibonacci 数列。
所以使得任意三条边都不能组成三角形的序列最小是个 Fibonacci 数列,我们设其为 \(f\)

因为 \(1\le a_i\le 2^{31}-1\),所以算出一个常数 \(k\) 使得 \(f_k\le 2^{31}-1\le f_{k+1}\),显然当树上点数 \(>k\) 时一定能组成三角形。
Fibonacci 数列增长速度极快,\(k\) 差不多是 \(40\) 左右。
所以说 \(\le k\) 时直接暴力即可啦。


Problem 5(滑动窗口):给定 \(n\) 个数和一个数 \(k\),求 \(\min(a_1,a_2,\dots,a_k)\)\(\min(a_2,a_3,\dots,a_{k+1})\)\(\dots\)\(\min(a_{n-k+1},a_{n-k+2},\dots,a_n)\) .

单调队列板子。
单调队列就是具有单调性的队列。

我们维护一个单调递增的队列。
进队模拟:

| |      <- 3
| 3 |    <- 2 因为将 2 插入后不单调递增了,所以删掉 3 
| 2 |    <- 4
| 2 4 |

最小值直接就是队头。
考虑滑动一步时的情景。
[3 2 4] 1 => 3 [2 4 1] .

原队列:| 2 4 | .
也就是删一个 3,加一个 1 .
发现 3 根本不在队头(不影响最小值)(它也不在队列里啊),所以不用操作。
然后加入 1,因为 1 插入后就不单调递增了,所以把 2 4 都弹出,队列变成 | 1 |,目前最小值也就是 1 了。

struct monotone_queue // 单调队列 
{
	static const int N=23333;
	int q[N],head,tail;
	monotone_queue(){head=1; tail=0;} // 初始化
	inline void pop(){++head;} // 弹出是直接弹出 
	inline void push(int x) // 插入要维护单调性 
	{
		while ((head<=tail)&&(q[tail]>x)) --tail; // 如果插进去后单调性不保证了,就不断弹出。
		q[++tail]=x; 
	}
//	inline void getmin(){return q[head];}
};

如果要维护最大值,就改成单调递减的单调队列即可。


Problem 6:给定 \(n\) 个数 \(a_1,a_2,\dots,a_n\),对于任意 \(1\le l\le r\le n\),求 \(a_l+a_{l+1}+\cdots+a_r\) 的最大/小值,\(1\le N\le 10^5\)

直接暴力是 \(O(n^3)\) 的。
加上前缀和优化,可以到达 \(O(n^2)\),但是还是过不了。
设这个前缀和为 \(s\),那么 \(a_l+a_{l+1}+\cdots+a_r=s_r-s_{l-1}\)
显然要使得 \(s_{l-1}\) 尽量小。
\(s\) 数组维护一个前缀最小值 \(m_i=\min(s_1,s_2,\cdots,s_i)\)
因为使得 \(s_{l-1}\) 尽量小,故对于每个 \(i\),它的最大贡献就是 \(s_r-m_{r-1}\),扫一遍即可。

当然也有一个比较漂亮的 贪心/dp 做法,这里就不说了。


Problem 7:给定 \(n\) 个数 \(a_1,a_2,\dots,a_n\ge0\),和一个数 \(k\),求满足 \(a_l+a_{l+1}+\cdots+a_r\le k\) 的最大长度。

\(s_i=a_0+a_1+a_2+\cdots+a_i\),也就是前缀和。
注意到每个数都大于等于 \(0\), 所以前缀和是单调递增的。

转换成前缀和形式,就是找到一个最大的 \(r\),使得 \(s_r-s_{l-1}\le k\) .
也就是 \(s_r\le k+s_{l-1}=c\) .
枚举个 \(l\),然后二分 \(r\) 可以做到 \(O(n\log n)\) .

注意到每个数都大于等于 \(0\),所以采用如下策略:

  • 先暴力出 \(l=1\) 的时候的情况 \(r\) 最多能到多少。
  • 考虑 \(l\) 右移一位,显然 \(r\) 可以补上几个数,也就让 \(r\) 一直右移,直到不满足条件为止。

这是个 \(O(n)\) 的算法。

Code:

int n,k,ans;
int main()
{
	scanf("%d%d",&n,&k);
	for (int i=1;i<=n;i++) scanf("%d",a+i);
	int sum=0;
	for (int l=1,r=1;l<=n;l++) // 第一次默认暴力 
	{
		while ((r<=n)&&(sum+z[r]<=k)){sum+=z[r]; ++r;} // 往右移 
		ans=max(ans,r-l); sum-=z[l];
	}
	return 0;
}

因为左右端点暴力移动次数不会超过 \(2n\),所以复杂度是 \(O(n)\) 的。


Problem 8:有 \(n\times n\) 的一个方阵,在其中还有 \(m\) 个特殊点。
两个点的「距离」定义为它们的曼哈顿距离。
任意一个点的「权值」为其到达每个特殊点的「距离」的最小值。
\((1,1)\) 走到 \((n,n)\),求经过的权值的最小值最大是多少。要求时间复杂度低于 \(O(n^3)\)

从每个特殊点开始广搜一遍,就能 \(O(n^2)\) 求每个点的权值了。

显然,存在一个 \(k\),使得权值为 \(k\) 时,能从 \((1,1)\) 走到 \((n,n)\),但是权值 \(\ge k+1\) 时,就无法走到
对于只走 \(\ge m\) 的权值时,直接暴力 check,就可以做到 \(O(n^2\log n)\) 了。

同 Problem 3,将格子从大到小加进去,存在一个时刻,\((1,1)\)\((n,n)\) 就联通了,答案就是那个格子的权值。
这个联通性用并查集维护即可,时间复杂度 \(O(n^2)\)

并查集:

维护 \(n\) 堆元素,
操作:

  1. \(i,j\) 合并为一堆
  2. 询问 \(i\)\(j\) 是否在同一堆

每个点都有一个箭头,初始每个点都指向自己。

\(i,j\) 合并就把 \(i\) 一直沿箭头走走到的点的箭头指向 \(j\)
查询就是查询从 \(i\) 一直沿箭头走走到的点与 \(j\) 一直沿箭头走走到的点是否相同。

Code:

const int N=10005;
int f[N],n;
int get(int p) // get 为了能看懂就不压成一行了= = 
{
	if (p==f[p]) return p;
	else return get(f[p]);
}
void merge(int p1,int p2) // 合并 
{
	p1=get(p1); p2=get(p2); // 把祖先合并 
	f[p1]=p2;
}
int main()
{
	cin>>n;
	for (int i=2;i<=n;i++) f[i]=i; // init
}

考虑并查集连成一条链,那么 get 的复杂度就是 \(O(n)\) 的,有亿点点慢。
考虑走的时候直接把路上的节点连到跟上,这就叫路径压缩。

get 函数修改如下:

int get(int p) // get 为了能看懂就不压成一行了= = 
{
	if (p==f[p]) return p;
	else return f[p]=get(f[p]);
}
posted @ 2020-10-08 11:58  Jijidawang  阅读(338)  评论(0编辑  收藏  举报
😅​