ybt 题目总结&吐槽 合集(中)

  • 第三章 图论
    • 最短路
      • T12 汽车加油
    • 强连通分量
      • T13 软件安装
      • T14 宫室宝藏
  • 第四章 数据结构
    • 树状数组
      • T15 区间修改区间查询
    • RMQ
      • T16 与众不同
      • T17 降雨量
      • T18 超级钢琴
    • 倍增
      • T19 开车旅行
      • T20 运输方案
      • T21 删边操作
  • 第五章 动态规划
    • 区间DP
      • T22 最小代价
    • 数位DP
      • T23 魔法数字

第三章 图论

最短路

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)在左上角,修改区域左上角为(x1,y1)(x1,y1)(x1,y1),右上角为(x2,y2)(x2,y2)(x2,y2),
区间修改就是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.f(x2+1,y2+1)+k,f(x1,y2+1)−k,f(x2+1,y1)−k,f(x1,y1)−k.
至于区间查询,我们先列出其原始表达式,即:∑i=1n∑j=1m∑k=1i∑l=1jf(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∑n​j=1∑m​k=1∑i​l=1∑j​f(i,j).考虑怎么优化这个过程,对于后两维可知,对于一个特定的f(x,y)f(x,y)f(x,y),每当k≥xk\geq xk≥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=1n∑j=1m((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∑n​j=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,pos−1)(k,l,pos-1)(k,l,pos−1)和(k,pos+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)删除元素,如果正序预处理,处理完就直接从链表中删除,就解决了这个问题。此题也给我们一个启发:对于同时要考虑下标的预处理,可以考虑用某种特定顺序的双向链表处理。
处理出两人开一次车的目的地后,就可以枚举点处理出开几次车的目的地同时解决开车距离的问题。然而这还不够快,考虑到这个图不存在修改,可以用倍增加速,记des(op,i,j)des(op,i,j)des(op,i,j)表示A/B先开车,从i出发,开2j次车的目的地,disa(op,i,j)disa(op,i,j)disa(op,i,j)表示A/B先开车,从i出发,小A开车的距离,disbdisbdisb同理。那么在预处理部分,就应该有如下的转移:

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生效,我们需要取走这范围以外的所有极值点,但是怎么取这个极值点又成为一个问题。
把整个思路翻转过来(这个更难想到),设dp[l][r][x][y]dp[l][r][x][y]dp[l][r][x][y]表示在[l,r][l,r][l,r]内取,留下的极值点是x和y的最小花费,ans(l,r)ans(l,r)ans(l,r)就通过这个dp加上上面花一次选择这两点的花费就可以求出。
显然这两个极值点不能在此之前取这区间的若干轮当中取走,所以更新这个dp的后两维也应该是x和y,那么这个时候按照区间dp的套路操作,只需枚举一个分界点,从更新左保留右和更新右保留左当中选一个最小的(对于此题不更新也是一种选择)就行了。极值也是枚举得到的,不过考虑到n≤50n\leq50n≤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=p1,p2,p3,...,pkS={p1,p2,p3,...,pk}S=p1,p2,p3,...,pk,p=lcm(S)p=lcm(S)p=lcm(S),则xmodpi(i∈[1,k])=(xmodp)modpi.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;
}

ybt 神(bian)奇(tai)题目总结合集(中)相关推荐

  1. ybt 神(bian)奇(tai)题目总结合集(上)

    ybt终于算是毕业了,不得不说ybt的变态题目实在太多了,当然没做出来的更变态.不愧是有变态oj ybt 题目总结&吐槽 合集(上) 第一章 基础算法 二分 T1 飞离地球 DFS T2 数独 ...

  2. ybt 神(bian)奇(tai)题目总结合集(下)

    ybt 题目总结&吐槽 合集(下) 第五章 动态规划 树形DP T24 权值统计 T25 树的合并 状压DP T26 涂抹果酱 T27 炮兵阵地 T28 最短路径 T29 图的计数 单调队列 ...

  3. 神乎奇技的播放软体-MPlayer

    神乎奇技的播放软体-MPlayer MPlayer是一款非常好用万能视频播放软件,几乎可以播放所有当前流行的视频格式(*.avi;*.as*;*.mp*;*.dat;*.m*v;*.n*v;*.qt; ...

  4. ptaa乘以b_PTA|团体程序设计天梯赛-练习题目题解锦集(C/C++)(持续更新中……)...

    C++ CPP C++语言开发 PTA|团体程序设计天梯赛-练习题目题解锦集(C/C++)(持续更新中--) PTA|团体程序设计天梯赛-练习题目题解锦集(持续更新中) 实现语言:C/C++:     ...

  5. PTA|团体程序设计天梯赛-练习题目题解锦集(C/C++)(持续更新中……)

    PTA|团体程序设计天梯赛-练习题目题解锦集(持续更新中) 实现语言:C/C++:      欢迎各位看官交流讨论.指导题解错误:或者分享更快的方法!! 题目链接:https://pintia.cn/ ...

  6. 计算机模拟贝特朗奇论,由贝特朗奇论谈几何概型中的等价转化

    <由贝特朗奇论谈几何概型中的等价转化>由会员分享,可在线阅读,更多相关<由贝特朗奇论谈几何概型中的等价转化(9页珍藏版)>请在人人文库网上搜索. 1.由贝特朗奇论谈几何概型中的 ...

  7. 题目:查找数组中的重复数字,要求空间复杂度为O(1)(基于Java实现)

    题目:查找数组中的重复数字,要求空间复杂度为O(1)(基于Java实现) 题目: 在一个长度为 n 的数组 nums 里的所有数字都在 0-n-1 的范围内.数组中某些数字是重复的,但不知道有几个数字 ...

  8. LeetCode面试必刷题目总结 持续更新中...

    说明 文章源地址:sanzo.top/#/post/算法与数据结构/算法题 多数元素 题目链接 找到数组中众数(出现次数>⌊n2⌋>\lfloor\frac{n}{2}\rfloor> ...

  9. 英语与计算机工作总结,2017年上学期英语教师个人工作总结与2017年上学期计算机教学工作总结合集.doc...

    2017年上学期英语教师个人工作总结与2017年上学期计算机教学工作总结合集 2017年上学期英语教师个人工作总结与2017年上学期计算机教学工作总结合集 2017年上学期英语教师个人工作总结 20* ...

最新文章

  1. matplotlib绘制多个子图
  2. boost::gil::generate_gaussian_kernel用法的测试程序
  3. .net 后台读取pdf的值
  4. 冒泡算法代码java_java版本的冒泡算法
  5. 第十三章 时间序列分析和预测
  6. android刷新时的圆形动画_Android自定义加载圈动画效果
  7. 前端学习(3267):js中this在类中的表现
  8. linux 下zip文件的压缩和解压
  9. 函数指针与回调函数详解
  10. Java匿名内部类里为什么能用外部变量
  11. 我们身边的知识产权单元测试答案(期末考试复习)【湘潭大学】
  12. 世界500百强企业中国的CEO对我们的忠告!!!!!我们要告别稚气了
  13. ARM Linux启动分析----head-armv.S内幕
  14. 2018年泰迪杯数据挖掘比赛c题
  15. 各种学习资料链接 干货 啃啃啃
  16. Arcgis 10.2 软件安装教程
  17. BFS - CH2906 - 武士风度的牛
  18. 实战Kaggle比赛(二)——房价预测
  19. 网络工程师必备 5款网络故障排除工具
  20. 专家,除了呼吁涨价你还会干点什么?

热门文章

  1. 苹果13系统锁屏延迟_iPhone锁屏有延迟怎么办 锁屏延迟问题解决方法
  2. 英雄之刃显示服务器断开怎么办,英魂之刃手游新手常见问题
  3. 乐教乐学各关的解(51-60)
  4. php计算多少小时多少分钟多少秒
  5. OpenCV(6):基于本地库的图像识别软件(批量读取图片)
  6. php留言本在线制作,Flash+php+mysql简单留言本制作
  7. Python青少年等级考试实操题(二级)
  8. 大争之世智造为基,瑞科智能将亮相第21届SIMM深圳机械展
  9. Sci-Fi 科幻迷们,爱死机第二季来啦 | Mixlab 科幻实验
  10. 汽车电子的发展简介和V型开发模式