ybt 题目总结&吐槽 合集(中)
第三章 图论
最短路
T12 汽车加油
(洛谷P4009)
此题是我的第一道紫题。鬼知道我当初经历了什么…
这道题写最短路的一个纠结的地方就在于怎么处理剩余油量,因为这个用边权表示比较困难。根据飞行路线的经验,我们可以把开车看作进入新的一层,如果这点是加油站,为了实现强行加油的效果,其下面k层中的任意一层在该点都只能回到这一层加满油重新走(把步数恢复),建设一个加油站同理。
最终构造出来的图形是一个n2层的图,每一层都是一个完整的图,代表一步的所有情况。
建图如下:
for(i = 1;i <= n;i++){
for(j = 1;j <= n;j++){
id[i][j] = ++top;
scanf("%d",&a[i][j]);
}
}
for(i = 1;i <= n;i++){
for(j = 1;j <= n;j++){
if(a[i][j]){
x = id[i][j],y = id[i][j] + n * n;//拿出这层和对应下一层的编号
if(j != n) save(x,y + 1,0);
if(j != 1) save(x,y - 1,B);
if(i != 1) save(x,y - n,B);
if(i != n) save(x,y + n,0);
for(l = 1;l <= k;l++) save(x + l * n * n,x,A);//加油站从k层内的任意一层跳回
continue;
}
for(l = 1;l <= k;l++){
x = id[i][j] + (l - 1) * n * n,y = id[i][j] + l * n * n;//对k步之内的全部位置建图
if(j != n) save(x,y + 1,0);
if(j != 1) save(x,y - 1,B);
if(i != 1) save(x,y - n,B);
if(i != n) save(x,y + n,0);
save(x,id[i][j],C + A);
if(l == k) save(y,id[i][j],C + A);//同理,只不过是可选的方案,仍然能往下走
}
}
}
剩下的部分就是一个普通的SPFA,没啥可说的。这题的分层图做法是网络流改的,大概也因此比较难想出来、难理解,除此之外BFS、记搜都可以做,可能在建图上简单一些,但是实现过程没有分层图这么直白。
强连通分量
T13 软件安装
(洛谷P2515)
这题首先一定要注意到每一个软件最多依赖一个软件的性质,因为这意味着如果由依赖指向被依赖建图,缩点去掉环之后所得的是一棵树,这题就变成一个带权的选课方案。这题代码复杂度是强连通分量造成的,然而恶心程度全是这个带权选课方案的问题
首先贴上这段的代码:
void dfs(int now){
int i,j,k,temp;
for(i = w2[now];i <= m;i++) dp[now][i] = c2[now];
for(k = head2[now];~k;k = e2[k].nxt){
temp = e2[k].to;
dfs(temp);
for(i = m - w2[now];i >= 0;i--){
for(j = 0;j <= i;j++){
dp[now][i + w2[now]] = max(dp[now][i + w2[now]],dp[now][i + w2[now] - j] + dp[temp][j]);
}
}
}
}
我们必须要注意的一个问题是,在更新父节点的时候,我们首先必须保证选了父节点。因此所有在当前节点now,花费代价小于w[now]的都不能被更新到,因此我们选择暴力加上w[now],这就保证了只更新不小于w[now]的部分(如果选择进行一些讨论改循环条件当然也可以,但是远远不如这么搞来的简单,万一错了还不容易发现,不要问我为什么知道)。
剩下的就是强联通分量的板子,不贴了。
T14 宫室宝藏
(洛谷P2403)
这题上来就会碰到此题最难的部分:建图。
首先很显然的一点是,我们只需要处理有传送门的房间,以下直接把有传送门的房间简称为房间了。
举例来讲,如果对于每一个横向传送,我们都向这一行全部的房间建边,那么这个建边自身就是
O
(
n
)
O(n)
O(n)的,肯定不行。那么不难想到开一些中转点,每一行的中转点向这行的所有房间连边。对于所有的横向传送门,只需要向这行的中转点建边,这就变成一个
O
(
1
)
O(1)
O(1)的建图。在这个建图的过程中比较容易想到中转点,然而比较容易忘记的是对中转点向这行其他种类的房间建边(至少我忘了,卡了我半个中午)。对于扩展传送门,暴力判一下周围是否有房间就行了,反正也是常数级。总结起来这题的建图思路就基本明确了,每行一个中转点,每列一个中转点,横向传送门连横向中转点,纵向传送门连纵向中转点,扩展传送门暴力连边,中转点连向同行/同列全部点。建图部分如下:
for(i = 1;i <= n;i++){
scanf("%d %d %d",&x[i],&y[i],&z[i]);
mp[x[i]][y[i]] = i;
}
for(i = 1;i <= n;i++){
if(z[i] == 1){
save1(i,n + x[i]);
save1(n + x[i],i);
save1(n + l + y[i],i);//建中转点注意不要把n和m搞反,否则编号会冲突
}
if(z[i] == 2){
save1(i,n + l + y[i]);
save1(n + l + y[i],i);
save1(n + x[i],i);
}
if(z[i] == 3){
for(j = 0;j < 8;j++){
if(mp[x[i] + dx[j]][y[i] + dy[j]]){
save1(i,mp[x[i] + dx[j]][y[i] + dy[j]]);
}
}
save1(n + l + y[i],i);//这个是最容易忘的!
save1(n + x[i],i);
}
}
剩下的就是强联通分量的板子,没有什么难度,此题的难度确实完全出现在存边方面。
第四章 数据结构
树状数组
T15 区间修改区间查询
(洛谷P4514)
本合集唯一上榜的模板题。
从一维的区间修改区间查询获得启发,再从二维前缀和获得启发,修改操作就很显然了,记差分数组
f
(
i
,
j
)
f(i,j)
f(i,j),
(
1
,
1
)
(1,1)
(1,1)在左上角,修改区域左上角为
(
x
1
,
y
1
)
(x1,y1)
(x1,y1),右上角为
(
x
2
,
y
2
)
(x2,y2)
(x2,y2),
区间修改就是
f
(
x
2
+
1
,
y
2
+
1
)
+
k
,
f
(
x
1
,
y
2
+
1
)
−
k
,
f
(
x
2
+
1
,
y
1
)
−
k
,
f
(
x
1
,
y
1
)
−
k
.
f(x2+1,y2+1)+k,f(x1,y2+1)-k,f(x2+1,y1)-k,f(x1,y1)-k.
f(x2+1,y2+1)+k,f(x1,y2+1)−k,f(x2+1,y1)−k,f(x1,y1)−k.
至于区间查询,我们先列出其原始表达式,即:
∑
i
=
1
n
∑
j
=
1
m
∑
k
=
1
i
∑
l
=
1
j
f
(
i
,
j
)
.
\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}\sum\limits_{k=1}^{i}\sum\limits_{l=1}^{j}f(i,j).
i=1∑nj=1∑mk=1∑il=1∑jf(i,j).考虑怎么优化这个过程,对于后两维可知,对于一个特定的
f
(
x
,
y
)
f(x,y)
f(x,y),每当
k
≥
x
k\geq x
k≥x时,每次改变i都会在循环中加一遍
f
(
x
,
y
)
f(x,y)
f(x,y),即从i这一维来讲
f
(
x
,
y
)
f(x,y)
f(x,y)出现了
(
n
−
x
+
1
)
(n-x+1)
(n−x+1)次。同理,加上另一维,可知
f
(
x
,
y
)
f(x,y)
f(x,y)在整个表达式当中会出现
(
n
−
x
+
1
)
(
m
−
y
+
1
)
(n-x+1)(m-y+1)
(n−x+1)(m−y+1)次。
打开括号,原式可以化简如下:
∑
i
=
1
n
∑
j
=
1
m
(
(
n
+
1
)
(
m
+
1
)
∗
f
(
i
,
j
)
−
i
∗
f
(
i
,
j
)
−
j
∗
f
(
i
,
j
)
+
i
∗
j
∗
f
(
i
,
j
)
)
\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}((n+1)(m+1)*f(i,j)-i*f(i,j)-j*f(i,j)+i*j*f(i,j))
i=1∑nj=1∑m((n+1)(m+1)∗f(i,j)−i∗f(i,j)−j∗f(i,j)+i∗j∗f(i,j))
这里要注意,涉及到的变量其实是四个,即
f
(
i
,
j
)
,
i
∗
f
(
i
,
j
)
,
j
∗
f
(
i
,
j
)
,
i
∗
j
∗
f
(
i
,
j
)
f(i,j),\, i*f(i,j),\, j*f(i,j),\, i*j*f(i,j)
f(i,j),i∗f(i,j),j∗f(i,j),i∗j∗f(i,j),所以需要用四个树状数组分别维护,查询的时候再汇总到一起。建议把四个的修改和查询在函数里面先搞完,否则看起来会异常的混乱。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long
using namespace std;
const int N = (1 << 11) + 1;
int n,m;
struct yjx{
long long p[N][N];
int lowbit(int x){
return x & (-x);
}
void add(int x,int y,int w){
int i,j;
for(i = x;i <= n;i += lowbit(i)){
for(j = y;j <= m;j += lowbit(j)){
p[i][j] += w;
}
}
}
long long query(int x,int y){
int i,j;
long long ret = 0;
for(i = x;i;i -= lowbit(i)){
for(j = y;j;j -= lowbit(j)){
ret += p[i][j];
}
}
return ret;
}
}A,Ai,Aj,Aij;
void Add(int x,int y,int w){//分别修改,注意树状数组要把加的值额外加一定倍数
A.add(x,y,w);
Ai.add(x,y,x * w);
Aj.add(x,y,y * w);
Aij.add(x,y,x * y * w);
}
long long Query(int x,int y){
return (x + 1) * (y + 1) * A.query(x,y) - (y + 1) * Ai.query(x,y) - (x + 1) * Aj.query(x,y) + Aij.query(x,y);
}
signed main(){
int z,w,x1,y1,x2,y2;
scanf("%lld %lld",&n,&m);
while(scanf("%lld",&z) != EOF){
if(z == 1){
scanf("%lld %lld %lld %lld %lld",&x1,&y1,&x2,&y2,&w);
Add(x1,y1,w);
Add(x1,y2 + 1,-w);
Add(x2 + 1,y1,-w);
Add(x2 + 1,y2 + 1,w);
}
if(z == 2){
scanf("%lld %lld %lld %lld",&x1,&y1,&x2,&y2);
printf("%lld\n",Query(x2,y2) - Query(x2,y1 - 1) - Query(x1 - 1,y2) + Query(x1 - 1,y1 - 1));
}
}
return 0;
}
RMQ
T16 与众不同
这题能够上榜主要在于ST表的灵活运用。
考虑怎么判断出一个不含有重复数字的区间,对于任何一个数x,记它上次出现的位置为last[x],显然在(last[x],x]之间的部分是可能成为答案的。记一个数组pre[i]表示以i为结尾的合法区间的开头位置,显然可以用last数组进行更新,pre[i]=max(pre[i-1],last[x]+1).通过这个,我们就能够求得所有以某点结尾的最长合法区间长度,ST表上正是要维护这一信息支持查询。
然而现在还存在一个问题,由于询问是在一整个区间内的,单独用ST表查询,很可能查询到比L小的位置,因此答案应该从两部分当中取最大值,一个是所有pre<L的部分,一个是pre>=L的部分,后者是从ST表内查得的,前者就是一个值。考虑到pre具有单调性,所以可以在区间内二分找这个分界点。
总之此题对于ST表运用确实很灵活,如果这题不在RMQ里面很有可能思路就偏到线段树去 (逛公园害人不浅) ,那问题复杂程度就会大很多。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 2e5 + 1;
const int M = 1e6;
int a[N],st[N][30],pre[N],last[2000002],dp[N],log[N],judge[2000002];
int find(int L,int R){
int mid,l = L,r = R + 1;
while(l < r){
mid = (l + r) >> 1;
if(pre[mid] < L) l = mid + 1;
else r = mid;
}
return l;
}
int main(){
int n,m,i,j,l,r,len,res;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
scanf("%d",&a[i]);
if(a[i] < 0) a[i] = M - a[i];
if(judge[a[i]]) last[a[i]] = judge[a[i]];
judge[a[i]] = i;
pre[i] = max(pre[i - 1],last[a[i]] + 1);//求解pre[i]
dp[i] = i - pre[i] + 1;
}
log[0] = -1;
for(i = 1;i <= n;i++){
log[i] = log[i >> 1] + 1;
st[i][0] = dp[i];
}
for(j = 1;j <= log[n];j++){
for(i = 1;i + (1 << j) - 1 <= n;i++){
st[i][j] = max(st[i][j - 1],st[i + (1 << (j - 1))][j - 1]);
}
}
for(i = 1;i <= m;i++){
scanf("%d %d",&l,&r);
++l,++r;
len = find(l,r),res = len - l;
if(len < r) res = max(res,max(st[r - (1 << log[r - len]) + 1][log[r - len]],st[len + 1][log[r - len]]));
printf("%d\n",res);
}
return 0;
}
T17 降雨量
(洛谷P2471)
堪称一道阴间题。
先考虑一下大概的做法:取区间(L,R)中的最大值,判断是否小于a[R],不满足就是错误,如果满足,看一下区间内是否有降雨量未知的,有就是可能,没有就是正确。
没了。
然而此题绝对没有看起来这么简单。
很快就会发现,这道题最麻烦的一个问题在于降雨量未知的年份。对于每一个已知降雨量的年份,像离散化那样处理一个id数组,如果R-L≠id[R]-id[L],就能判断出[L,R]存在降雨量未知的年份。然而另一个问题在于,给定的L和R可能降雨量未知,为了求区间最大值,需要把L和R改到这个区间内离这两个年份最近的两年,求这个区间的最大值(因为扩大范围显然可能致错,而缩小范围无非是去掉一些没意义的部分),这可以用二分解决。
吐槽自己一句,此处如果老老实实用lower_bound和upper_bound,这道题不至于卡我两个半小时…
值得注意的是,我们求的范围是(L,R),所以ST表查询的范围理论上应该是[L+1,R-1],但考虑到一些年份没有降雨量,所以如果某个端点的降雨量是未知的,就不能再+1/-1.考虑清楚以上要素,不把二分写错并且明确分清这几个区间,就能完成这一步。
接下来考虑怎么判断。首先先排除几个非法情况:R>L显然是非法的。根据定义,设(L,R)的最大降雨量是A,L/R年的降雨量已知且小于A,是非法的;a[L]<a[R],也是非法的。
除去这些情况,如果L或R任意一年的降雨量未知,或者这几年当中存在降雨量未知的年份,都是可能的;
剩下的都是合法的情况。
可以看出来,如果立足于端点是否已知,那么讨论会变得很混乱,但如果采取一种一种筛的话,顺序合理就能把讨论简化不少,但是在第一次做的时候思路是否能如此清晰绝对是一个问题(至少我没做到)。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
map<int,int> id;
int n,st[50001][16],a_id[50001];
int findL(int L){
int mid,l = 1,r = n + 1;
while(l < r){
mid = (l + r) >> 1;
if(a_id[mid] >= L) r = mid;
else l = mid + 1;
}
return l;
}
int findR(int R){
int mid,l = 0,r = n;
while(l < r){
mid = (l + r + 1) >> 1;
if(a_id[mid] > R) r = mid - 1;
else l = mid;
}
return l;
}
int main(){
int i,j,q,y,l,r,templ,tempr,L,R,len,res,Log[50001];
scanf("%d",&n);
for(i = 1;i <= n;i++){
scanf("%d %d",&a_id[i],&y);
id[a_id[i]] = i,st[i][0] = y;
}
Log[0] = -1;
for(i = 1;i <= n;i++) Log[i] = Log[i >> 1] + 1;
for(j = 1;j <= Log[n];j++){
for(i = 1;i + (1 << j) - 1 <= n;i++){
st[i][j] = max(st[i][j - 1],st[i + (1 << (j - 1))][j - 1]);
}
}
scanf("%d",&q);
for(i = 1;i <= q;i++){
scanf("%d %d",&L,&R);
if(R <= L){
printf("false\n");
continue;
}
l = findL(L),r = findR(R);
templ = l + 1,tempr = r - 1;
if(!id[R]) ++tempr;
if(!id[L]) --templ;
len = Log[tempr - templ + 1];
if(id[L] && id[R] && st[l][0] <= st[r][0]){
printf("false\n");
continue;
}
res = max(st[templ][len],st[tempr - (1 << len) + 1][len]);
if((id[L] && res >= st[l][0]) || (id[R] && res >= st[r][0])) printf("false\n");
else{
if(id[R] - id[L] != R - L || !id[R] || !id[L]) printf("maybe\n");
else printf("true\n");
}
}
return 0;
}
/*
8
-8 1
-2 2
0 62
1 5
2 7
13 1
18 10
19 14
6
-19 -2
-2 -1
-18 -2
8 12
1 2
2 13
*/
T18 超级钢琴
(洛谷P2048)
这道题有一个关键的不同于一般题的地方:一个音符可以多次选,但是方案不能重复。因此问题就变为取k个长度在[L,R]之间的和弦的最大价值和。
现在的问题就在于怎么找这k个和弦,首先这题的长度要求是一个区间,所以用一个三元组
(
k
,
l
,
r
)
(k,l,r)
(k,l,r),表示一个以k为左端点,右端点在
[
l
,
r
]
[l,r]
[l,r]之间的和弦,其价值就是这段和弦的最大价值。这个价值可以用ST表预处理出来。
另外,由于上面提到的,一个音符可以多次选,那么一个三元组
(
k
,
l
,
r
)
(k,l,r)
(k,l,r)产生的解就不一定只有一个最优的,所以每次选完一个三元组,假设选择它的点在pos,还要把
(
k
,
l
,
p
o
s
−
1
)
(k,l,pos-1)
(k,l,pos−1)和
(
k
,
p
o
s
+
1
,
r
)
(k,pos+1,r)
(k,pos+1,r)(当然前提是确实存在这个三元组)加入。这一来对ST表的要求就变高了,不仅要知道最大值,还要知道最大值出现的位置。虽然这一般出现的比较少(至少我做这题之前没见过),其实也不难办,只需要知道最大值出现在哪一部分,就知道应该在的位置,这个最大值的上传一个道理。对于取k个三元组,用堆维护三元组的信息即可。
这题见过之后难度可能确实不大,但初次做难度确实大,主要是以此题扩充一下ST表的使用技巧和范围,和上面T16上榜理由基本一致。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<functional>
#include<utility>
using namespace std;
typedef pair<int,pair<int,pair<int,pair<int,int> > > > pr;
priority_queue<pr> Q;
int x,y,st[500001][21],pos[500001][21],a[500001],sum[500001],Log[500001];
void Get(int s,int l,int r){
int lg,temp,p,t;
lg = Log[r - l + 1];
if(st[l][lg] > st[r - (1 << lg) + 1][lg]) x = st[l][lg] - sum[s - 1],y = pos[l][lg];
else x = st[r - (1 << lg) + 1][lg] - sum[s - 1],y = pos[r - (1 << lg) + 1][lg];
}
int main(){
int i,j,n,k,L,R,l,r,t,p,q,s;
long long res = 0;
scanf("%d %d %d %d",&n,&k,&L,&R);
for(i = 1;i <= n;i++){
scanf("%d",&a[i]);
sum[i] = sum[i - 1] + a[i];
st[i][0] = sum[i];
pos[i][0] = i;
}
Log[0] = -1;
for(i = 1;i <= n;i++) Log[i] = Log[i >> 1] + 1;
for(i = 1;i <= Log[n];i++){
for(j = 1;j + (1 << i) - 1 <= n;j++){
st[j][i] = max(st[j][i - 1],st[j + (1 << (i - 1))][i - 1]);
if(st[j][i - 1] > st[j + (1 << (i - 1))][i - 1]) pos[j][i] = pos[j][i - 1];
else pos[j][i] = pos[j + (1 << (i - 1))][i - 1];//顺着st一起更新
}
}
for(i = 1;i + L - 1 <= n;i++){
l = i + L - 1,r = min(i + R - 1,n);
Get(i,l,r);
Q.push(make_pair(x,make_pair(y,make_pair(i,make_pair(l,r)))));
//从左到右依次是:三元组(i,l,r)最大价值,出现该价值的右端点,i,l,r.
}
for(i = 1;i <= k;i++){
res += Q.top().first;
t = Q.top().second.first;
s = Q.top().second.second.first;
l = Q.top().second.second.second.first;
r = Q.top().second.second.second.second;
if(!Q.empty()) Q.pop();
if(t < r){
Get(s,t + 1,r);
Q.push(make_pair(x,make_pair(y,make_pair(s,make_pair(t + 1,r)))));
}
if(t > l){
Get(s,l,t - 1);
Q.push(make_pair(x,make_pair(y,make_pair(s,make_pair(l,t - 1)))));
}
}
printf("%lld\n",res);
return 0;
}
/*
10 13 3 7
595
384
-435
-197
-677
661
4
-100
-653
220
ans=1112
*/
倍增
T19 开车旅行
(洛谷P1081)
又是一道真正的阴间题,在这个合集里都是T0的存在。
此题需要求解两个问题,一是求最小的小A/小B的开车路程,二是求从每个点出发两人各自的开车路程,这显然要做一个预处理。
首先是怎么处理两人开一次车的目的地的问题,如果把高度进行排序,假设排序后的编号为pos,那么这个目的地只会从编号比它大的{pos-2,pos-1,pos+1,pos+2}里产生两个。单独来看4个取2个固然不难,但是暴力判断编号是否大于它就不太容易,因为如果确实存在编号小于它的,还需要再补一个,最终要保证是从4个里面选择。
此处处理的方法,就是使用双向链表。双向链表和单向比起来,最大的优势在于支持O(1)删除元素,如果正序预处理,处理完就直接从链表中删除,就解决了这个问题。此题也给我们一个启发:对于同时要考虑下标的预处理,可以考虑用某种特定顺序的双向链表处理。
处理出两人开一次车的目的地后,就可以枚举点处理出开几次车的目的地同时解决开车距离的问题。然而这还不够快,考虑到这个图不存在修改,可以用倍增加速,记
d
e
s
(
o
p
,
i
,
j
)
des(op,i,j)
des(op,i,j)表示A/B先开车,从i出发,开2j次车的目的地,
d
i
s
a
(
o
p
,
i
,
j
)
disa(op,i,j)
disa(op,i,j)表示A/B先开车,从i出发,小A开车的距离,
d
i
s
b
disb
disb同理。那么在预处理部分,就应该有如下的转移:
for (i = 1; i <= n; i++) {
if (f[i]) {//存在最近的点
des[0][i][0] = f[i];
disa[0][i][0] = abs(a[pos[i]].h - a[pos[f[i]]].h);
disb[0][i][0] = 0;
}
if (f2[i]) {//存在第二近的点
des[1][i][0] = f2[i];
disa[1][i][0] = 0;
disb[1][i][0] = abs(a[pos[i]].h - a[pos[f2[i]]].h);
}
}
在预处理之外的部分,需要考虑好一个问题,对于任意大于2的2x,由于是小A先开车,在开了2(x-1)次车之后开车的仍然是小A,而如果是2,就应该换成是小B开车,因此在这个时候后半段的转移的op应该对1异或。求两者开车的路程和终点的过程同理,如果这一次开车的次数是20,显然应该换成另一个人的目的地。考虑清楚这一部分,倍增其实就不难写了。
这题其实一直存在一个比较容易翻车的点,就是区分开未排序的原序列和排了序的链表序列,链表用的是排序的序列的下标,而除此之外的信息都是存在原序列的,在做这题的过程当中,至少我是混淆了非常多次(以至于改了七八个巨大的bug输出一点儿都没变),所以对于这种两组下标互相对应(也包括数组里面0/1的区分)的题,需要考虑好定义,避免混淆。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 1;
struct yjx{
int id,h,nxt,pre;
}a[N];
int n,m,lgn,pos[N],f[N],f2[N];
long long disa[2][N][21],disb[2][N][21],des[2][N][21],lena,lenb;
//A/B先开车,从i开车2^j次的结果
bool cmp(yjx x,yjx y){
return x.h < y.h;
}
int sec(int now,int c1,int c2){
if(!c1) return a[c2].id;
else if(!c2) return a[c1].id;
else{
if(a[now].h - a[c1].h <= a[c2].h - a[now].h) return a[c1].id;
else return a[c2].id;
}
}
void del(int now){
if(a[now].nxt) a[a[now].nxt].pre = a[now].pre;
if(a[now].pre) a[a[now].pre].nxt = a[now].nxt;
}
void find(int now,int l){
int i,temp = now,k = 0;
lena = lenb = 0;
for(i = lgn;i >= 0;i--){
if(des[k][temp][i] && disa[k][temp][i] + disb[k][temp][i] <= l){
l -= disa[k][temp][i] + disb[k][temp][i];
lena += disa[k][temp][i],lenb += disb[k][temp][i];
if(!i) k ^= 1;//更换开车的人
temp = des[k][temp][i];
}
}
}
int main(){
int i,j,k,l,p,p1,p2,sp,x,y;
long long sa,sb;
scanf("%d",&n);
for(i = 1;i <= n;i++){
scanf("%d",&a[i].h);
a[i].id = i;
}
sort(a + 1,a + n + 1,cmp);
for(i = 1;i <= n;i++){
pos[a[i].id] = i;
a[i].pre = i - 1;
a[i].nxt = i + 1;
}
a[1].pre = a[n].nxt = 0;
for(i = 1;i < n;i++){
p = pos[i],p1 = a[p].pre,p2 = a[p].nxt;
if(p1 && (a[p].h - a[p1].h <= a[p2].h - a[p].h || !p2)){
f2[i] = a[p1].id,f[i] = sec(p,a[p1].pre,p2);
}
else{
f2[i] = a[p2].id,f[i] = sec(p,p1,a[p2].nxt);
}
del(p);
}
for(i = 1;i <= n;i++){
if(f[i]){
des[0][i][0] = f[i];
disa[0][i][0] = abs(a[pos[i]].h - a[pos[f[i]]].h);
disb[0][i][0] = 0;
}
if(f2[i]){
des[1][i][0] = f2[i];
disa[1][i][0] = 0;
disb[1][i][0] = abs(a[pos[i]].h - a[pos[f2[i]]].h);
}
}
while((1 << (lgn + 1)) <= n) ++lgn;
for(j = 1;j <= lgn;j++){
for(i = 1;i <= n;i++){
for(k = 0;k <= 1;k++){
l = k;
if(j == 1) l ^= 1;//更换后半段开车的人
if(des[k][i][j - 1]) des[k][i][j] = des[l][des[k][i][j - 1]][j - 1];
if(des[k][i][j]){
disa[k][i][j] = disa[k][i][j - 1] + disa[l][des[k][i][j - 1]][j - 1];
disb[k][i][j] = disb[k][i][j - 1] + disb[l][des[k][i][j - 1]][j - 1];
}
}
}
}
sa = 1,sb = 0,sp = 0;
scanf("%d",&m);
for(i = 1;i <= n;i++){
find(i,m);
if(!lenb) lena = 1;//按照题意,只要B开车路程为0,视作相等的无穷大。要注意审题
if(sa * lenb > sb * lena || sa * lenb == sb * lena && a[pos[i]].h > a[pos[sp]].h){
//比较比值大小要转为比较积,老生常谈的问题了
sa = lena,sb = lenb,sp = i;
}
}
printf("%d\n",sp);
scanf("%d",&m);
for(i = 1;i <= m;i++){
scanf("%d %d",&x,&y);
find(x,y);
printf("%lld %lld\n",lena,lenb);
}
return 0;
}
T20 运输方案
(洛谷P2680)
又是一道阴间题。
首先,此题求的是最大权值最小,有一个免费的操作,很快能想到二分答案。
现在的问题就在于,怎么验证一个答案是否正确。求两点间距离当然可以用LCA,很明显的一点是,如果一段路径的权值大于mid,必须把这个虫洞放在这个路径上,这一来我们就需要找一个方法让所有权值大于mid的路径都被这个虫洞覆盖到。问题就变成如何找这个位置。
这里就出现一个很奇妙的方法:树上差分。
具体来说,把两个端点+1,LCA-2,从叶节点向根节点上传这个差分,每一个点的权值就相当于经过这点的路径数。如果某点的权值等于经过某点的路径数,且这条边权确实不少于需要减少的权值,就去掉这一条边,mid就是成立的。以及,考虑到一个子树内的dfs序连续,为了实现快速上传,需要按照dfs序倒序上传,这就保证了父节点一定在上传前就已经被更新完。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 3e5 + 1;
struct yjx{
int nxt,to,c;
}e[N << 1];
struct wyx{
int st,ed,dist,anc;
}a[N];
int ecnt = -1,n,q,time,head[N],dfn[N],rnk[N],dep[N],siz[N],dis[N],f[N][21],val[N];
void save(int x,int y,int w){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
e[ecnt].c = w;
head[x] = ecnt;
}
void dfs(int now,int fa){
int i,temp;
dfn[now] = ++time,rnk[time] = now,siz[now] = 1,dep[now] = dep[fa] + 1;
for(i = 1;i <= 20 && (1 << i) <= dep[now];i++){
f[now][i] = f[f[now][i - 1]][i - 1];
}
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
dis[temp] = dis[now] + e[i].c;
f[temp][0] = now;
dfs(temp,now);
siz[now] += siz[temp];
}
}
bool judge(int x,int y){
return dfn[y] >= dfn[x] && dfn[y] <= dfn[x] + siz[x] - 1;
}
int lca(int x,int y){
int i;
if(dep[x] < dep[y]) swap(x,y);
if(judge(y,x)) return y;
for(i = 20;i >= 0;i--){
if(f[x][i] && !judge(f[x][i],y)) x = f[x][i];
}
return f[x][0];
}
bool check(int mid){
int i,cnt = 0,ret = 0;
memset(val,0,sizeof(val));
for(i = 1;i <= q;i++){
if(a[i].dist > mid){
++val[a[i].st],++val[a[i].ed],val[a[i].anc] -= 2;//处理差分
ret = max(ret,a[i].dist - mid);
cnt++;
}
}
if(!cnt) return 1;
for(i = n;i >= 1;i--){
val[f[rnk[i]][0]] += val[rnk[i]];//上传权值,注意区分rnk和dfn
}
for(i = 2;i <= n;i++){
if(val[i] == cnt && dis[i] - dis[f[i][0]] >= ret) return 1;
}
return 0;
}
int main(){
int i,j,x,y,w,l,r,mid,mx = 0;
scanf("%d %d",&n,&q);
memset(head,-1,sizeof(head));
for(i = 1;i < n;i++){
scanf("%d %d %d",&x,&y,&w);
save(x,y,w),save(y,x,w);
}
dfs(1,0);
for(i = 1;i <= q;i++){
scanf("%d %d",&a[i].st,&a[i].ed);
a[i].anc = lca(a[i].st,a[i].ed);
a[i].dist = dis[a[i].st] + dis[a[i].ed] - 2 * dis[a[i].anc];
mx = max(mx,a[i].dist);
}
l = 0;r = mx;
while(l < r){
mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n",l);
return 0;
}
T21 删边操作
此题综合性非常强。换句话说,代码非常长…
首先需要知道一个性质:两棵树合并为一棵树,其新的直径的两个端点一定是这两棵树原来的直径的端点(这可以用反证法证明,很好证)。因此每一次合并两棵树的时候,都需要取出它们一共的四个端点,从6种组合当中选一个最大的作为直径,同时更新这棵树的端点。由于已知的是合并哪两个点,为了知道点属于哪一棵树,最快的方法就是并查集。最后更新答案的时候,除以原来两棵树的直径再乘上新树的直径就完成了。此题的倍增只是用来求LCA然后求树上路径长度用的,我甚至懒得把这段放出来了。
核心代码如下:
int main(){
int i,n,q,x,y,l1,r1,l2,r2,l,r;
long long temp;
scanf("%d",&n);
memset(head,-1,sizeof(head));
res[1] = 1;
for(i = 1;i <= n;i++){
scanf("%d",&w[i]);
diam[i][0] = diam[i][1] = i;//直径预处理
res[1] = (res[1] * w[i]) % mod;//结果预处理
}
for(i = 1;i < n;i++){
scanf("%d %d",&a[i].from,&a[i].to);
save(a[i].from,a[i].to),save(a[i].to,a[i].from);
}
dis[1] = w[1];
dfs(1,0);//预处理树上信息,用来求LCA和树上距离
for(i = n;i >= 2;i--){
scanf("%d",&id[i]);
}
for(i = 1;i <= n;i++) p[i] = i;
for(i = 2;i <= n;i++){
temp = 0;
x = find(a[id[i]].from),y = find(a[id[i]].to);//并查集
l1 = diam[x][0],r1 = diam[x][1];
l2 = diam[y][0],r2 = diam[y][1];
if(temp < dist(l1,r1)) temp = dist(l1,r1),l = l1,r = r1;
if(temp < dist(l1,l2)) temp = dist(l1,l2),l = l1,r = l2;
if(temp < dist(l1,r2)) temp = dist(l1,r2),l = l1,r = r2;
if(temp < dist(r1,l2)) temp = dist(r1,l2),l = r1,r = l2;
if(temp < dist(r1,r2)) temp = dist(r1,r2),l = r1,r = r2;
if(temp < dist(l2,r2)) temp = dist(l2,r2),l = l2,r = r2;
temp %= mod;//选择新的端点和直径
res[i] = res[i - 1] * temp % mod * ksm(dist(l1,r1) % mod,mod - 2) % mod * ksm(dist(l2,r2) % mod,mod - 2) % mod;
//快速幂求逆元
p[find(y)] = find(x);
diam[p[x]][0] = l,diam[p[x]][1] = r;//更新直径的信息
}
for(i = n;i >= 1;i--) printf("%lld\n",res[i]);
return 0;
}
第五章 动态规划
区间DP
T22 最小代价
(洛谷P5336)
又一道T0阴间题。
此题最大的难点在于,每一次取出一部分成绩单,剩下的会合起来,然后居然可以跨过这个部分再取一段连续的,这完全不同于一般区间DP的套路。于是我就寄了
冷静地思考一下这个题,每取一次成绩单,产生的贡献为a+b*(max-min)2,这个max和min我们其实可以自行决定(这是本题的关键),为了使得这个max和min生效,我们需要取走这范围以外的所有极值点,但是怎么取这个极值点又成为一个问题。
把整个思路翻转过来(这个更难想到),设
d
p
[
l
]
[
r
]
[
x
]
[
y
]
dp[l][r][x][y]
dp[l][r][x][y]表示在
[
l
,
r
]
[l,r]
[l,r]内取,留下的极值点是x和y的最小花费,
a
n
s
(
l
,
r
)
ans(l,r)
ans(l,r)就通过这个dp加上上面花一次选择这两点的花费就可以求出。
显然这两个极值点不能在此之前取这区间的若干轮当中取走,所以更新这个dp的后两维也应该是x和y,那么这个时候按照区间dp的套路操作,只需枚举一个分界点,从更新左保留右和更新右保留左当中选一个最小的(对于此题不更新也是一种选择)就行了。极值也是枚举得到的,不过考虑到
n
≤
50
n\leq50
n≤50,时间完全是够的。另外,鉴于w过大,需要离散化。
总之此题需要抛开区间DP的一些定式思维和固定套路(即使是倒序复活这种高级一些的也算),分析这题的核心内容,从而设计出dp,然而这个设计过程确实极难想到,属于是我见过的区间DP里面的技巧天花板级别了。
记搜部分代码如下:
long long dfs(int l,int r){
if(~dp[l][r]) return dp[l][r];
if(l > r) return dp[l][r] = 0;
dp[l][r] = 2e9;
int i,j,k;
for(i = 1;i <= m;i++){
for(j = i;j <= m;j++){
for(k = l;k < r;k++){
f[l][r][i][j] = min(f[l][r][i][j],min(dfs(l,k) + f[k + 1][r][i][j],min(f[l][k][i][j] + dfs(k + 1,r),f[l][k][i][j] + f[k + 1][r][i][j])));
dp[l][r] = min(dp[l][r],f[l][r][i][j] + a + b * (d[i] - d[j]) * (d[i] - d[j]));
}
}
}
return dp[l][r];
}
数位DP
T23 魔法数字
数位DP目前没有开一个专题总结,主要是数位DP比较套路,但大概以后会有的。此题除了数位DP的套路,还有一些新意,所以就被选中了。
首先有一个性质:设一个集合
S
=
p
1
,
p
2
,
p
3
,
.
.
.
,
p
k
S={p1,p2,p3,...,pk}
S=p1,p2,p3,...,pk,
p
=
l
c
m
(
S
)
p=lcm(S)
p=lcm(S),则
x
m
o
d
p
i
(
i
∈
[
1
,
k
]
)
=
(
x
m
o
d
p
)
m
o
d
p
i
.
x \,\,mod\,\, p_i(i \in [1,k])=(x\,\,mod\,\,p)\,\,mod\,\,p_i.
xmodpi(i∈[1,k])=(xmodp)modpi.可能写的不是很严谨,用自然语言来说,一个数模上某集合的一个元素前,对该集合的最小公倍数取模,答案不变。利用这一性质,我们就不必保留每一位数取模的结果,只需保留对2520(1~9的公倍数)取模的余数,最后再判断即可。
考虑一下要开的数组dp,pos表示位,left表示余数,stat表示已经取的数的集合(状压实现),那么数组大小是195122520,根据这个数组大小估算时间复杂度,有些偏大。这个还是只能从余数角度考虑,由于一个数能否整除5,只需看最后一位是不是5或者0,这个信息在记搜中很容易传递,所以可以从集合中去掉5,改为对504取模,这一来就足以通过。
剩下的就是套路了。
总之此题兼具数学优化和状压,算是不那么模板化的数位DP,考虑到数位DP自带高难度,这算是一道难题,但同时也很有意思。以一个不那么阴间的题结尾真是极好的
记搜部分代码如下:
long long dfs(int pos,int left,int stat,int num,bool go){
if(!pos){
int i,cnt = 0;
for(i = 1;i <= 9;i++){
if(i != 5 && stat & (1 << (i - 1)) && left % i == 0) ++cnt;
}
if(stat & 16 && (num == 0 || num == 5)) ++cnt;//16即(1<<(5-1))
return cnt >= k;
}
if(~dp[pos][stat][left] && go) return dp[pos][stat][left];
int i,p = 9;
long long temp,ret = 0;
if(!go) p = st[pos];
for(i = p;i >= 0;i--){
temp = (left + i * mi[pos] % mod) % mod;
if(i) ret += dfs(pos - 1,temp,stat | (1 << (i - 1)),i,(go || i != p));
else ret += dfs(pos - 1,temp,stat,i,(go || i != p));
}
if(go) dp[pos][stat][left] = ret;
return ret;
}