一个写博客有史以来最大的单篇工程。
首先这13套题并不是每一套都当比赛打了,因为有几套的题之前几乎都已经做完了,实际上当模拟赛打的应该只有13,14,16,17,18,19,20这七套,考虑到早年间的联赛是两天六道题,所以每一套题我都取难度最高的四道(相当于把当时的签到题扔了,毕竟我觉得现在不会考普及难度的签到题了)当一套联赛题做。不得不感慨这个难度的上升速度,命题风格也确实变化很大,不过就当是面对不同的风格的卷子多点儿准备吧。
命题的风格直接决定题目难度分配,现在题目难度不按顺序排列之后,难度的分配更灵活,也更考验考试运营。
在比较早的几年的联赛(起码是到16年),卷子的风格基本都是一两道签到题(毕竟我人为扔了几道),一道大难题(思维和实现难度都大,早年间这个任务基本都是树上倍增完成的),剩下的都是中档题,有些考细节,有些考实现,有些考思路,有些很综合。总体上感觉大部分是动态规划或者贪心。
这其中也有没有大难题但是平均难度高的,比如14.
从差不多17年开始,大难题大幅度增加,正解越来越难写,暴力越来越高级,数据结构和思维题越来越多,可以达到平均难度是紫题的水平。有些年份是以算法和数据结构技巧提高难度,有些年份是以实现提高难度,有些是以思维提高难度,当然也有混合三者的。
所以说,根据今年的初赛,14年这样的卷子放在今天讨论就没啥意义了,重点还是在于17年开始的这种写一堆暴力然后思考个别的正解的打法。至于早年间的题目,虽然整场的考试不会再有这样的难度,但是一些套路是值得总结的。
首先,一道题总是有一个突出的难点,我觉得基本都能归为以下三个元素:实现,技巧,思维。
近几年的考试题目技巧全都是拉满的水平,有那么几年思维也是拉满的水平 (20年出来挨打) 。按照我的考场规划,一般来说惯例开局遍历,这个时候就基本能分析出一道题的大体难度:正解有多难,暴力有多难(是多难而不是多难写),暴力大致可以得多少分。这三个部分其实完全不一样,有的题正解很难但是暴力好想且能得很多分,有的题想暴力的时候其实已经出正解了,这就是规划的问题。
总体上,正解很难而暴力相对简单的题一定是排首位的,因为这类题倒腾30~50分钟之后就不值得回头再看了,一轮暴力就可以直接塞进文件夹了;正解不是很难想但是要借助比较高级的技巧或者难实现,暴力不是很好写的这类题放在中间,这种题就比较关键,可以看情况选择先写个暴力等第二轮再想正解,或者直接写完暴力写正解,看时间和心态。排到最后的基本就是一些暴力都很难写的随缘的东西。
第二轮的目标大致有两种,一种是优化空间大的,另一种是有思路的,我偏向先后者。
考虑到联赛有270分钟,如果没有严重的运营失误导致在一道题上花费了过多的时间打暴力,基本足够完成一个开局定下来的计划。现在问题就是怎么避免发生严重的运营失误。
首先是必须要避免读题的时候因为错过信息误判难度,比如21年初赛T2(现 身 说 法);其次是对于一道题必须把实现难度考虑进去,比如儒略日;最后是有很多题的几档暴力关联不一定就是一个优化到下一个,有些是割裂的,这时候其实真不一定就是分多的难写,毕竟有思维难度的介入,比如17年的逛公园,在想清楚的情况下topo比最短路计数写起来稳很多。
运营是一场考试的基础,如果运营崩了确实影响非常严重(仍然是现身说法)。排除一些可能出现的严重失误,剩下的就是怎么看题之后规划一个合理的运营,那么这个问题就转移到:
时间分配其实只是一个运营的课题,精力和心态都是应该考虑到运营当中的(尤其是后者)。基本上来说大部分想好的暴力只需要25 ~ 35分钟怎么都写完了,关键是debug的时间,这个就看细节究竟做的如何,而细节的东西除了练习以外也要看状态,所以一开始先把所有确定的分数比较少的暴力写了进入状态,这个过程其实一般需要90 ~ 120分钟,剩下的就是研究一些高分的暴力(对于我一般都是dp),这个时候状态会好很多,而且心态会更稳定一点,即使研究不出来对状态影响会比较小(因为再剩下的都是些不知道能不能做的题)。比如很多人都习惯上来做签到题,但是我往往习惯第三个看最简单的一道题/模拟,效果确实很好。尤其是这道题是一道模拟的时候,有时候实现想不了太清楚,心里可能确实没底,但是放第三个做时间不是很紧张而且心态比较稳,这时候做这种题就好一些(比如17年的时间复杂度我就是打完了列队和逛公园的暴力之后做的)。
另外一个就是时间和心态的关系。通常来讲我对一道题有一个期望用时,如果差不多到了时间打了暴力仍然没研究出来就放弃/打暴力,而不会接着研究下去,这样保全了整体的运营不受影响,但是心态上可能受到一些影响。一般来说是不会受什么影响的,但是在今年初赛打完之后我对此有了新的认识,因为T2白给60分确实影响太大了,这说明这个策略虽然没有什么问题,但是对于这个期望时间的设计就是一个考验。我个人的心态还是比较好的,大部分时候不会紧张到操作严重变形。假如在时间分配上合理,那么怎么具体的处理每一道题就是目前的问题了。
其实不同的题型之间做的过程差不太多,正解一般其实都是优化暴力的时候想到的(当然也有打着打着暴力灵光乍现的时候),只是对于一些算法的实现需要额外注意,比如贪心这种主要看思维正确性的算法,比如一些数据结构是否会有偏差(之前就遇到过一次线段树上二分很难写,换成树状数组上二分就很好写,这个过程花了35分钟),模拟写的时候会不会忽略掉什么致命细节,debug毕竟总还是耗时的一部分。另外对于我自己,感觉很大的一点还是不要感觉难写就不写,其实有时候觉得麻烦实际写起来不会非常复杂,有些代码长是因为它就应该是这么长的,实际写起来很快。时间的估测固然是一方面,但是有时候时间足够的前提下应该多想,想清楚实现思路就写一写,只要安排得当(最次有Plan B也行吧),总还是可以写出来一些东西的。
#1 有一些题的部分分特殊限制很多,这种题千万不要忘了数组到底应该分别开多大。
#2 注意有些时候要使用unsigned long long,有些时候要使用__int128。
#3 离散化的时候一定要注意最终离散化之后的下标到底怎么对应。
#4 不要把几个经典的模型记串了 (线段覆盖有4种,你知道吗)
#5 注意搜索的状态恢复,不要把搜索、dp、贪心的过程混为一谈。
#6 多测不清空,下场见祖宗
#7 在做题的时候,要善于转化模型,前提是便于自己的思考和正确性。
#8 不要在puts()
里面打\n
。
#9 不要把bool
类型的变量当int
用,也不能当int
输出。
#10 注意看好一道题的字符限制,不要把同时有大小写的题当成只有大/小写。
#11 注意字符串长度的限制,以及现在不能用gets()
了。
#12 注意一道题是否有SPJ (某些人曾经对着构造题思考为什么与样例输出不一样,我不说是谁)
#13 在数学问题当中,注意对计算精度的控制(说白了就是少用除法),因为手动调精度真的太烦人了。
#14 审 好 题
这里抽取了一些写的过程中印象比较深的题。重点不在于这道题的解,在于怎么分析并做出来这道题,以及一些过程中遇到的问题。
这题一个比较容易想到的做法就是先进行bfs,然后像线段覆盖那样考虑取哪些水塔。
问题是到底从城市开始搜还是从水塔开始搜?
如果从城市开始搜,一座城市覆盖的线段是不连续的,这非常难搞,如果从水塔开始搜,那么也可能出现不连续的情况。但是如果仔细思考一下就能发现这两者不一样,后者只有在有一些城市不能被覆盖的时候才会出现线段不连续的情况,而如果城市都能被覆盖一定不存在线段不连续的情况。因此合理的做法是从水塔出发bfs,先假设存在方案记录区间。
证明:一个城市 ( x , y ) (x,y) (x,y) 如果被覆盖,只能被左右和上面的流水覆盖。反证,假设一个水流覆盖城市 ( x , y − 1 ) (x,y-1) (x,y−1) 和 ( x , y + 1 ) (x,y+1) (x,y+1) 而不覆盖 ( x , y ) (x,y) (x,y) ,那么覆盖的过程中一定需要越过 ( x − 1 , y ) (x-1,y) (x−1,y) 。因为 ( x , y ) (x,y) (x,y) 不能从左右覆盖,只能从上面被覆盖,那么这个时候这股流水一定能覆盖这个城市,假设不成立,原命题成立。
现在就是一个标准的线段覆盖,求最少的区间覆盖这一段。
这个时候就需要区分一下,因为线段覆盖的模型太多了,包括选择最多不交叉线段(借用礼堂)、求最少集合使得集合内线段不交叉(牛棚分配),求最少点数使得所有线段至少包括一个点(雷达安装),这道题(浇灌草坪),以及长的很像的模拟(廊桥分配)。
我自己做这题一开始当雷达安装做了,然后差点挂大分233
首先按左端点排序。假设现在选取到一个右端点,记录这个位置,所有左端点比这个小的理论都可以只选一个,只更新右端点的位置就行了,超出这个区间的时候就不能归为一个集合了,退出这一次循环并重新取右端点。
注意这题的线段覆盖不是要求左右端点重合而是相邻。
#include
#include
#include
#include
#include
using namespace std;
typedef pair<int,int> pr;
const int N = 501;
struct yjx{
int L,R;
}s[N];
bool cmp(yjx x,yjx y){
return x.L < y.L;
}
queue<pr> Q;
int n,m,l,r,st,cnt,a[N][N],vis[N][N];
bool judge[N];
int dx[4] = {0,1,0,-1};
int dy[4] = {1,0,-1,0};
void bfs(int sx,int sy){
int i,x,y,xx,yy;
l = m + 1,r = 0;
if(vis[sx][sy]) return;
Q.push(make_pair(sx,sy));
while(!Q.empty()){
x = Q.front().first,y = Q.front().second;
Q.pop();
if(vis[x][y] == st) continue;
vis[x][y] = st;
if(x == n){
l = min(l,y),r = max(r,y);
cnt += (!judge[y]);
judge[y] = 1;
}
for(i = 0;i < 4;i++){
xx = x + dx[i],yy = y + dy[i];
if(xx < 1 || xx > n || yy < 1 || yy > m || vis[xx][yy] == st || a[xx][yy] >= a[x][y]) continue;
Q.push(make_pair(xx,yy));
}
}
}
int main(){
int i,j,pos = 0,temp;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
for(j = 1;j <= m;j++){
scanf("%d",&a[i][j]);
}
}
for(st = 1;st <= m;st++){
bfs(1,st);
s[st].L = l,s[st].R = r;
}
if(cnt < m){
printf("0\n%d\n",m - cnt);
return 0;
}
else{
cnt = 0,i = 1;
sort(s + 1,s + m + 1,cmp);
while(pos < m){
++cnt;
temp = pos;
while(s[i].L == m + 1) ++i;
for(;s[i].L <= temp + 1 && i <= m;i++){
if(s[i].L == m + 1) continue;
pos = max(pos,s[i].R);
}
}
printf("1\n%d\n",cnt);
}
return 0;
}
这题的正解就是照题意搜索,根据字典序,从左下到右上,先右移后左移。
剪枝其实也非常简单,就是如果相邻两块颜色一样就不移动。
没了。
很明显这道题的重点就是考察怎么实现和逻辑,基本需要实现这么几个功能,一是左右交换,二是下落,三是消除。按照游戏的过程,整个过程应该是交换—下落—消除—下落—消除—…,如此循环,直到不能消除。
所以把这三个部分分别用函数实现就行了,唯一值得一说的细节就是消除,由于相连的块也可以消掉(样例已经给了),所以一次三消消掉的块需要额外标记,不能直接移除,等到全部消完再一起移除。
另外注意这题的输入方式,一行的输入可能是8个,所以开数组的时候注意一下,否则会被卡掉40.
此题我觉得另一个阴间的地方就是输入的行列和如图的行列是反的,中间就因为这个错了一次。
代码如下:
#include
#include
#include
#include
using namespace std;
int n,G[6][8],lst[6][6][8],res[6][3],vis[6][8];
bool check(){
int i;
for(i = 0;i < 5;i++){
if(G[i][0]) return 0;
}
return 1;
}
void save(int s){
int i,j;
for(i = 0;i < 5;i++){
for(j = 0;j < 7;j++){
lst[s][i][j] = G[i][j];//记忆的时候不要忘了记层数
}
}
}
bool match(){
int i,j;
bool flag = 0;
for(i = 0;i < 5;i++){
for(j = 0;j < 7;j++){
if(G[i][j]){
if(i > 0 && i < 4 && G[i][j] == G[i - 1][j] && G[i][j] == G[i + 1][j]){
vis[i][j] = vis[i - 1][j] = vis[i + 1][j] = 1;
flag = 1;
}
if(j > 0 && j < 6 && G[i][j] == G[i][j - 1] && G[i][j] == G[i][j + 1]){
vis[i][j] = vis[i][j - 1] = vis[i][j + 1] = 1;
flag = 1;
}
}
}
}
if(flag){
for(i = 0;i < 5;i++){
for(j = 0;j < 7;j++){
if(vis[i][j]) vis[i][j] = 0,G[i][j] = 0;
}
}
}
return flag;
}
void update(){
int i,j,temp;
for(i = 0;i < 5;i++){
temp = 0;
for(j = 0;j < 7;j++){
if(!G[i][j]) ++temp;
else{
if(!temp) continue;
G[i][j - temp] = G[i][j],G[i][j] = 0;
}
}
}
}
void push(int x,int y,int d){
swap(G[x][y],G[x + d][y]);
update();
while(match()) update();
}
void dfs(int s){
if(check()){
int i;
for(i = 1;i < s;i++){
printf("%d %d %d\n",res[i][0],res[i][1],res[i][2]);
}
exit(0);
}
if(s == n + 1) return;
save(s);
int i,j,k,l;
for(i = 0;i < 5;i++){
for(j = 0;j < 7;j++){
if(G[i][j]){
if(i < 4 && G[i][j] != G[i + 1][j]){
push(i,j,1);
res[s][0] = i,res[s][1] = j,res[s][2] = 1;
dfs(s + 1);
res[s][0] = res[s][1] = res[s][2] = 0;
for(k = 0;k < 5;k++){
for(l = 0;l < 7;l++){
G[k][l] = lst[s][k][l];
}
}
}
if(i > 0 && G[i][j] != G[i - 1][j]){
push(i,j,-1);
res[s][0] = i,res[s][1] = j,res[s][2] = -1;
dfs(s + 1);
res[s][0] = res[s][1] = res[s][2] = 0;
for(k = 0;k < 5;k++){
for(l = 0;l < 7;l++){
G[k][l] = lst[s][k][l];
}
}
}
}
}
}
}
int main(){
int i,j;
scanf("%d",&n);
for(i = 0;i <= 5;i++){
for(j = 0;j <= 7;j++){
scanf("%d",&G[i][j]);
if(!G[i][j]) break;
}
}
dfs(1);
puts("-1");
return 0;
}
一道逻辑比较长的贪心题。
首先要捋清这题的逻辑:如果车到站了,没有乘客在等候,那么车就直接走,否则就等着,所以不存在乘不上车的乘客。
因此假设到达 x x x 站的时间是 i n x in_x inx ,那么出站的时间就是这个站最晚到站的乘客和车的时间的最小值,到下一站的时间再加上 d x d_x dx 。
现在考虑一下怎么计算 d x − 1 d_x-1 dx−1 的贡献,如果车到某个站点之后需要等乘客,那么这个贡献对之后的人就没有用了,而乘客到站的时间不变,所以这个贡献只需要算一下在这段路之后下车的乘客个数就可以了。枚举这段路,把后面所有能产生贡献的到站时间减少就行了。
总之此题如果只是讲解题思路和逻辑并不算难,但是在实际写的时候需要一次性捋清这个逻辑,这并非一件非常容易的事情,做出来这道题确实需要比较好的状态和思路。
代码如下:
#include
#include
#include
using namespace std;
const int N = 1e3 + 1;
const int M = 1e4 + 1;
struct yjx{
int from,to,t;
}a[M];
int dis[N],lst[N],in[N],out[N],R[N],off[N];
int main(){
int i,j,k,n,m,p,num,cnt,pos;
long long res = 0;
scanf("%d %d %d",&n,&m,&p);
for(i = 1;i < n;i++){
scanf("%d",&dis[i]);
}
for(i = 1;i <= m;i++){
scanf("%d %d %d",&a[i].t,&a[i].from,&a[i].to);
lst[a[i].from] = max(lst[a[i].from],a[i].t);
++off[a[i].to];
}
for(i = 1;i <= n;i++){
in[i] = out[i - 1];
if(i ^ 1) in[i] += dis[i - 1];
out[i] = max(in[i],lst[i]);
}
//for(i = 1;i <= n;i++) printf("%d %d\n",in[i],out[i]);
for(i = 1;i <= p;i++){
cnt = 0;
for(j = 2;j <= n;j++){
if(!dis[j - 1]) continue;
num = 0;
for(k = j;k <= n;k++){
num += off[k];
if(in[k] <= lst[k]) break;
}
if(num > cnt){
cnt = num,pos = j;
}
}
//printf("%d\n",pos);
--dis[pos - 1];
for(j = pos;j <= n;j++){
--in[j];
if(in[j] < lst[j]) break;
}
}
for(i = 1;i <= m;i++){
res += in[a[i].to] - a[i].t;
//printf("%d %d\n",a[i].to,in[a[i].to]);
}
printf("%lld\n",res);
return 0;
}
每年一道大阴间题的习俗就是从这一年开始的,标准的树上倍增。
值得一说的是,这年考了两道倍增,另一道就是著名的倍增“模板”题开车旅行。
这道题求时间尽可能短,所以需要我们做一个二分答案,二分军队移动的步数。
首先一个非常明显的性质是,在时间最短的前提下,最理想的情况就是军队直接把所有根节点的子节点占领了。考虑到这题没有修改,可以倍增地把军队全部尽可能向上移动,最高到达根节点的子节点,并且记录一下这组军队还能走多少步。如果军队到不了根节点的子节点,就直接停在原地并覆盖子树。在处理完这一轮之后,可能还有一些子树是不能被覆盖的,首先扫一下哪些子树没有被覆盖(注意这时候不考虑还能移动的军队),根据前面的贪心可知这个是需要上传的。处理出这些点之后,先看自己的子树内有没有能够覆盖自己的,没有就依次用剩余时间多的覆盖距离远的子节点就行了。
虽然说的过程比较简单,然而实际上实现的过程非常恶心,先是倍增的预处理,在check函数中需要先上跳,然后dfs并处理出未覆盖子树以及对应的距离和点,再进行一轮贪心的匹配,代码极长,参与的变量极多,debug极其恶心(这套卷我没有当模拟做,这题我做+debug花了三个多小时),由于还有16年的天天爱跑步(这题我甚至还没怎么研究明白),堪称17年神仙打架之前第二阴间的题。
代码如下:
#include
#include
#include
using namespace std;
const int N = 5e4 + 1;
struct yjx{
int nxt,to;
long long c;
}e[N << 1];
struct wyx{
int pos;
long long d;
}sol[N],tre[N],left[N];
bool cmp(wyx x,wyx y){
return x.d > y.d;
}
int n,m,ecnt = -1,cnt,tot,head[N],dep[N],f[N][21];
int st[N];
long long dis[N][21];
bool vis[N];
void save(int x,int y,long long w){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
e[ecnt].c = w;
head[x] = ecnt;
}
void pre(int now,int fa){//标准的倍增预处理
int i,temp;
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];
dis[now][i] = dis[now][i - 1] + dis[f[now][i - 1]][i - 1];
}
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
f[temp][0] = now;
dis[temp][0] = e[i].c;
pre(temp,now);
}
}
bool check(int now){//找未覆盖的子树
int i,temp;
bool flag = 1,leaf = 1;
if(vis[now]) return 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == f[now][0]) continue;
leaf = 0;
if(!check(temp)){
flag = 0;
if(now == 1) tre[++cnt].pos = temp,tre[cnt].d = dis[temp][0];
//贪心,只记录根节点的子节点
else return 0;
}
}
if(leaf) return 0;
else return flag;
}
bool solve(long long mid){
int i,j,x,r = 1;
long long temp;
cnt = tot = 0;
memset(vis,0,sizeof(vis));
memset(left,0,sizeof(left));
//多测不清空,下场见祖宗
for(i = 1;i <= m;i++){//上跳
x = st[i],temp = mid;
for(j = 20;j >= 0;j--){
if(f[x][j] > 1 && dis[x][j] <= temp){
temp -= dis[x][j],x = f[x][j];
}
}
if(f[x][0] == 1 && temp >= dis[x][0]){//保存还能移动的军队
sol[++tot].pos = i,sol[tot].d = temp - dis[x][0];
if(sol[tot].d < left[x].d || !left[x].pos){
left[x].pos = i,left[x].d = sol[tot].d;
}
}
else vis[x] = 1;
}
if(check(1)) return 1;
else{
sort(sol + 1,sol + tot + 1,cmp);
sort(tre + 1,tre + cnt + 1,cmp);//按距离排序
memset(vis,0,sizeof(vis));
vis[0] = 1;
for(i = 1;i <= cnt;i++){
if(!vis[left[tre[i].pos].pos]){//看自己子树的点是否用了
vis[left[tre[i].pos].pos] = 1;
continue;
}
while(r <= tot && (vis[sol[r].pos] || sol[r].d < tre[i].d)) ++r;
//贪心找最大的和最大的匹配
if(r > tot) return 0;//时间不够覆盖,无解
vis[sol[r].pos] = 1;
}
return 1;
}
}
int main(){
int i,x,y;
long long l,r,mid,w;
memset(head,-1,sizeof(head));
scanf("%d",&n);
for(i = 1;i < n;i++){
scanf("%d %d %lld",&x,&y,&w);
save(x,y,w),save(y,x,w);
}
pre(1,0);
scanf("%d",&m);
for(i = 1;i <= m;i++) scanf("%d",&st[i]);
l = 0,r = 1e15;
while(l < r){
mid = (l + r) >> 1;
if(solve(mid)) r = mid;
else l = mid + 1;
}
if(l <= 1e15) printf("%lld\n",l);
else puts("-1");
return 0;
}
这年的唯一一道蓝题,然而其实比那几个绿题要简单。
这题比较值得一说的就是这个解多项式的方法:把一个多项式先去掉常数项,剩下的部分可以全部除以x,这样这个问题的复杂度就降低一维,这就把一个含log的问题变成了线性的(后来我才知道这叫秦九韶公式)。
#include
#include
#include
using namespace std;
const int mod = 19260817;
inline long long read(){
long long x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = ((x << 3) + (x << 1) + (c ^ 48)) % mod;
c = getchar();
}
return x * f;
}
const int N = 101;
int n,res[N];
long long a[N];
bool solve(int x){
int i;
long long val = a[n] * x % mod;
for(i = n - 1;i >= 1;i--){
val = (val + a[i]) * x % mod;
}
val = (val + a[0]) % mod;
return (val == 0);
}
int main(){
int i,m,cnt = 0;
scanf("%d %d",&n,&m);
for(i = 0;i <= n;i++){
a[i] = read();
}
for(i = 1;i <= m;i++){
if(solve(i)) res[++cnt] = i;
}
printf("%d\n",cnt);
for(i = 1;i <= cnt;i++) printf("%d\n",res[i]);
return 0;
}
一个细节相当多的题。
首先这题是个dp不难看出来,设 f ( i , j ) f(i,j) f(i,j)表示当前横坐标是 i i i,纵坐标是 j j j的最少点击次数。
此题有很多转移的限制,逐个分析一下:
在没有管子的时候,同一横坐标能多次点击,那么应该可以从自己转移,也就是 f ( i , j ) = m i n { f ( i , j − c i ) , f ( i − 1 , j − c i ) } + 1. f(i,j)=min\{f(i,j-c_i),f(i-1,j-c_i)\}+1. f(i,j)=min{f(i,j−ci),f(i−1,j−ci)}+1. 最后还有一个下落,即 f ( i , j ) = m i n { f ( i , j ) , f ( i , j + w i ) } . f(i,j)=min\{f(i,j),f(i,j+w_i)\}. f(i,j)=min{f(i,j),f(i,j+wi)}.
补充:此题不同于大部分dp,根据题意这题是不能从0开始转移的。
另一个问题就是高度不会超过 m m m(不是不能),一个比较直接的思路就是当 j > m j>m j>m 的时候强行赋值为 m m m ,或者保留这个高度,但是把超过m的最小值与 f ( i , m ) f(i,m) f(i,m) 取最小值,如果采取后者记得把数组开大点。
最后就是这个水管的问题,注意水管的这一格不能转移,建议手动±1.这部分和之前一样,为了简单,直接把水管覆盖的部分暴力赋成inf就行了。
另外,假设没有解的话需要从后往前扫,经过管子的数目预处理就可以了。
总之这题细节挺多的,比较看实现的水平和审题能力(我一开始就以为鸟的高度不能超过m,然后gg),如果写的时候实现的不好应该能体会到像这个游戏一样的高血压。
代码如下:
#include
#include
#include
using namespace std;
const int N = 1e4 + 1;
const int M = 2e3 + 1;
int f[N][M],up[N],down[N],sum[N],w[N],c[N];
int main(){
int i,j,n,m,k,x,y,z,res = 1e9 + 1;
scanf("%d %d %d",&n,&m,&k);
for(i = 1;i <= n;i++){
scanf("%d %d",&c[i],&w[i]);
}
for(i = 1;i <= k;i++){
scanf("%d %d %d",&x,&y,&z);
down[x] = y + 1,up[x] = z - 1;
}
for(i = 1;i <= n;i++){
if(down[i]) sum[i] = sum[i - 1] + 1;
else sum[i] = sum[i - 1];
}
memset(f,0x3f,sizeof(f));
for(i = 1;i <= m;i++) f[0][i] = 0;
for(i = 1;i <= n;i++){
for(j = c[i] + 1;j <= m + c[i];j++){
f[i][j] = min(f[i][j - c[i]] + 1,f[i - 1][j - c[i]] + 1);
}
for(j = m + 1;j <= m + c[i];j++) f[i][m] = min(f[i][m],f[i][j]);
for(j = 1;j <= m - w[i];j++){
f[i][j] = min(f[i][j],f[i - 1][j + w[i]]);
}
for(j = 1;j < down[i];j++) f[i][j] = 1e9 + 1;
if(down[i]) for(j = up[i] + 1;j <= m;j++) f[i][j] = 1e9 + 1;
}
for(i = 1;i <= m;i++){
res = min(res,f[n][i]);
}
if(res < 1e9) printf("1\n%d\n",res);
else{
for(i = n;i >= 1;i--){
for(j = m;j >= 1;j--){
if(f[i][j] < 1e9){
printf("0\n%d\n",sum[i]);
return 0;
}
}
}
}
return 0;
}
大阴间题。
首先说一句,根据一些人的经验,这题对于会玩斗地主的人反而不是很友好,毕竟这里面没有对手(我一个不会玩斗地主的听说能把王炸拆了带牌都觉得离谱),就挺离谱的。
首先这题最大的问题就是怎么区分贪心和搜索,即使是任何一种扑克牌游戏都不会的人也应该明白应该先进行一堆顺子和带牌操作再把剩下的打单双,这部分就没必要去搜索,但是到底是先带牌还是先顺子、顺子的单双三顺子到底有没有先后,这个经过一些仔细的思考(或者生活经验)就能举出一堆反例,也就是说这个部分是没法贪心的。
另外一个就是怎么拆牌的问题,这个好像又可以贪心了,什么时候应该拆什么;事实上这个还真能进行一定的决策,但是其实没必要,这部分也能搜索。
对于我而言,搜索的时候很容易让我产生一种惯性思维:搜索的过程暗含了决策的顺序。事实上这话没毛病,比如字典序就是一种决策顺序,搜的顺序还能用来剪枝,但是这不代表这种决策顺序是一种贪心。如果忽略这一点很容易忘记自己完全可以无脑搜索,而把代码写的非常非常恶心。
此外拆牌其实是两方面的,一种是拆牌打顺子,一种是拆牌打带牌,这两个注意区分,要用两层dfs分开做,否则又会把代码写的很恶心。
综上所述,这题必要的贪心只有最后打单双,剩下的全是搜索的工作,总体上是dfs顺子里面套dfs带牌,拆牌无脑拆,每次拆完都判断一下这种情况全部带牌需要出多少次牌。大小王如果同时出现,直接在外层分是否直接打了两种情况搜索就完事了。所以这种题永远要相信暴搜,起码分清楚贪心和剪枝。
代码如下:
#include
#include
#include
using namespace std;
int res,tot,group[5],c[21];
int add(){//处理带牌,能带则带
int i,ret = 0,temp[5];
for(i = 1;i <= 4;i++) temp[i] = group[i];
while(group[4]){
if(group[1] < 2 && !group[2]) break;
if(group[1] >= 2) --group[4],group[1] -= 2,++ret;
else if(group[2] >= 2) --group[4],group[2] -= 2,++ret;
else if(group[2] == 1) --group[4],group[2] -= 1,++ret;
}
while(group[3]){
if(!group[1] && !group[2]) break;
if(group[1]) --group[3],--group[1],++ret;
else if(group[2]) --group[3],--group[2],++ret;
}
ret += group[1] + group[2] + group[3] + group[4];
for(i = 1;i <= 4;i++) group[i] = temp[i];
return ret;
}
void split(){
tot = min(tot,add());
if(group[3]){
--group[3],++group[1],++group[2];
split();
++group[3],--group[1],--group[2];
}
if(group[4]){
--group[4],++group[2],++group[2];
split();
++group[4],--group[2],--group[2];
}
if(group[4]){
--group[4],++group[1],++group[3];
split();
++group[4],--group[1],--group[3];
}
}
void solve(int s){//三种顺子
memset(group,0,sizeof(group));
int i,j,k;
for(i = 2;i <= 16;i++) ++group[c[i]];
tot = 1e9;
split();//每一种状态先拆牌+带牌
res = min(res,s + tot);
for(i = 3;i <= 10;i++){
for(j = i;j <= 14;j++){
if(!c[j]) break;
if(j - i + 1 < 5) continue;
for(k = i;k <= j;k++) --c[k];
solve(s + 1);
for(k = i;k <= j;k++) ++c[k];
}
}
for(i = 3;i <= 12;i++){
for(j = i;j <= 14;j++){
if(c[j] < 2) break;
if(j - i + 1 < 3) continue;
for(k = i;k <= j;k++) c[k] -= 2;
solve(s + 1);
for(k = i;k <= j;k++) c[k] += 2;
}
}
for(i = 3;i <= 10;i++){
for(j = i;j <= 14;j++){
if(c[j] < 3) break;
if(j - i + 1 < 2) continue;
for(k = i;k <= j;k++) c[k] -= 3;
solve(s + 1);
for(k = i;k <= j;k++) c[k] += 3;
}
}
}
int main(){
int i,n,x,y,t,trump;
scanf("%d %d",&t,&n);
while(t--){
res = 1e9;
memset(c,0,sizeof(c));
for(i = 1;i <= n;i++){
scanf("%d %d",&x,&y);
if(!x){
if(y == 1) ++c[15];
if(y == 2) ++c[16];
}
if(x == 1) x = 14;
++c[x];
}
trump = min(c[15],c[16]);
if(trump){
--c[15],--c[16];
solve(1);
++c[15],++c[16];
}
solve(0);
printf("%d\n",res);
}
return 0;
}
一道期望题,看着很吓人,实际上并没有那么难。
首先这题必须搞清楚的一个重点就是申请换教室的时候显然不知道能不能成功,所以每一个结果不仅对应了一个确定的方案,而且也是对应了一种确定的概率,结果和概率是绑定的。
考虑 f ( i , j ) f(i,j) f(i,j) 表示到第 i i i 次课申请了 j j j 次换课上课花费的期望最少时间,如果这样设计就会发现转移其实是错的,因为申请失败和不申请并不是一个情况(这就是上面提到的结果和概率绑定),所以正确的dp应该设计成 f ( i , j , 0 / 1 ) f(i,j,0/1) f(i,j,0/1) ,最后一维表示是否申请更换教室。
这一来转移方程就很好想了,以 f ( i , j , 0 ) f(i,j,0) f(i,j,0)为例,应该是
f ( i , j , 0 ) = m i n { f ( i , j , 0 ) , f ( i − 1 , j , 0 ) + d i s c i − 1 , c i , f ( i − 1 , j , 1 ) + d i s c i − 1 , c i ∗ ( 1 − p i − 1 ) + d i s d i − 1 , c i ∗ p i − 1 ) } f(i,j,0)=min\{f(i,j,0),f(i-1,j,0)+dis_{c_{i-1},c_i},f(i - 1,j,1)+dis_{c_{i-1},c_i} * (1 - p_{i - 1}) + dis_{d_{i-1},c_i} * p_{i - 1})\} f(i,j,0)=min{f(i,j,0),f(i−1,j,0)+disci−1,ci,f(i−1,j,1)+disci−1,ci∗(1−pi−1)+disdi−1,ci∗pi−1)} ,另一种更复杂一些但是也差不多。
代码如下:
#include
#include
#include
using namespace std;
const int N = 2e3 + 1;
const int M = 301;
int c[N],d[N];
double p[N],f[N][N][2],dis[M][M];
int n,m,u,v;
int main(){
int i,j,k,x,y,w;
double d00,d01,d10,d11;
double res = 1e18;
scanf("%d %d %d %d",&n,&m,&u,&v);
for(i = 1;i <= n;i++) scanf("%d",&c[i]);
for(i = 1;i <= n;i++) scanf("%d",&d[i]);
for(i = 1;i <= n;i++) scanf("%lf",&p[i]);
for(i = 1;i <= u;i++){
for(j = 1;j <= u;j++){
dis[i][j] = 1e18;
}
}
for(i = 1;i <= v;i++){
scanf("%d %d %d",&x,&y,&w);
dis[x][y] = dis[y][x] = min(dis[x][y],1.0 * w);
}
for(i = 1;i <= u;i++) dis[i][i] = 0;
for(k = 1;k <= u;k++){
for(i = 1;i <= u;i++){
for(j = 1;j <= u;j++){
dis[i][j] = min(dis[i][j],dis[i][k] + dis[k][j]);
}
}
}
for(i = 1;i <= n;i++){
for(j = 0;j <= m;j++){
f[i][j][0] = f[i][j][1] = 1e18;
}
}
f[1][0][0] = f[1][1][1] = 0;
for(i = 2;i <= n;i++){
d00 = dis[c[i - 1]][c[i]],d01 = dis[c[i - 1]][d[i]],d10 = dis[d[i - 1]][c[i]],d11 = dis[d[i - 1]][d[i]];
f[i][0][0] = f[i - 1][0][0] + d00;
for(j = 1;j <= m && j <= i;j++){
f[i][j][0] = min(f[i][j][0],min(f[i - 1][j][0] + d00,f[i - 1][j][1] + d00 * (1 - p[i - 1]) + d10 * p[i - 1]));
f[i][j][1] = min(f[i][j][1],min(f[i - 1][j - 1][0] + d00 * (1 - p[i]) + d01 * p[i],f[i - 1][j - 1][1] + d00 * (1 - p[i - 1]) * (1 - p[i]) + d10 * p[i - 1] * (1 - p[i]) + d01 * (1 - p[i - 1]) * p[i] + d11 * p[i - 1] * p[i]));
}
}
for(i = 0;i <= m;i++) res = min(res,min(f[n][i][0],f[n][i][1]));
printf("%.2lf\n",res);
return 0;
}
一道字符串模拟。
首先感谢C++强大的库函数,这题没有sscanf我不知道该怎么写233
这题的关键信息不算很多,如果起点大于终点就不进入下面的嵌套,否则如果终点是n才增加一维复杂度;如果F多于E或者E多于F非法,如果嵌套当中重复用变量非法,且未参与循环的部分也占用变量。
具体的在代码中解释更方便一些。
#include
#include
#include
#include
#include
using namespace std;
typedef pair<int,int> pr;
const int N = 101;
stack<pr> S;
char s[N],s1[N],s2[N],s3[N];
bool vis[27];
int main(){
int i,n,t,ac,x,y,fail,ans,res,lipu,ch1,ch2,ch3;
scanf("%d",&t);
while(t--){
res = 0,lipu = 0,ac = 0,fail = 0;
memset(vis,0,sizeof(vis));
scanf("%d ",&n);
scanf("%s",s + 1);
if(s[3] == '1') ans = 0;
else{
ans = 0;
x = strlen(s + 1);
for(i = 5;i <= x;i++){
if(s[i] < '0' || s[i] > '9') break;
ans = ans * 10 + s[i] - '0';
}//这地方也可以用sscanf
}
for(i = 1;i <= n;i++){
scanf("%s",s + 1);
if(s[1] == 'F'){
scanf("%s %s %s",s1,s2,s3);
ch1 = s1[0] - 'a' + 1;
if(s2[0] != 'n') sscanf(s2,"%d",&ch2);
else ch2 = 101;
if(s3[0] != 'n') sscanf(s3,"%d",&ch3);
else ch3 = 101;//提取三个信息
if(vis[ch1]) lipu = 1;//变量重复
vis[ch1] = 1;
if(ch2 < 101 && ch3 == 101) ++ac,S.push(make_pair(ch1,1));
//合法循环,更新层数标记并放入栈
else if(ch2 > ch3) ++fail,S.push(make_pair(ch1,0));
//不可进入的循环,增加fail标记,也放入栈
else S.push(make_pair(ch1,2));
//O(1)循环
if(!fail) res = max(ac,res);
//当前循环可进入就尝试更新答案
}
if(s[1] == 'E'){
if(S.empty()) lipu = 1;//E多于F
else{
x = S.top().first,y = S.top().second;
vis[x] = 0;
if(!y) --fail;
else if(y == 1) --ac;
//取消这层循环的影响
S.pop();
}
}
}
if(!S.empty()) lipu = 1;//F多于E
while(!S.empty()) S.pop();//清空
if(lipu) puts("ERR");
else if(res == ans) puts("Yes");
else puts("No");
}
return 0;
}
这道题本来应该出现在底下的部分分分类,但是考虑到这题的正解我确实整明白了,就一起写一下。
50分暴力:考虑到询问只有500行,所以只需要维护这500行和最后一列的信息就够了,这里需要离散化,设原来的 x i x_i xi 离散后对应 x x i xx_i xxi 就可以了,注意最后一列的信息在维护的时候是不能离散化的。
代码如下:
#include
#include
#include
using namespace std;
#define mid (l + r >> 1)
const int N = 5e4 + 1;
const int M = 501;
const int N1 = 3e5 + 1;
int i,n,m,pos;
long long a[M][N],X[N1],XX[M],Y[N1],id[N1],col[N],w;
int main(){
int n1,q,j,x,y;
long long temp;
scanf("%d %d %d",&n,&m,&q);
for(i = 1;i <= q;i++){
scanf("%lld %lld",&X[i],&Y[i]);
id[i] = X[i];
}
if(n > 1){
for(i = 1;i <= n;i++) col[i] = i * m;
sort(id + 1,id + q + 1);
n1 = unique(id + 1,id + q + 1) - id - 1;
for(i = 1;i <= q;i++){
XX[i] = lower_bound(id + 1,id + n1 + 1,X[i]) - id;
}
//这个离散化里面要注意X,XX,id这三个数组的关系
for(i = 1;i <= q;i++){
for(j = 1;j <= m;j++){
a[XX[i]][j] = (X[i] - 1) * m + j;
}
}
for(i = 1;i <= q;i++){
temp = a[XX[i]][Y[i]];
printf("%lld\n",temp);
for(j = Y[i];j < m;j++) a[XX[i]][j] = a[XX[i]][j + 1];
for(j = X[i];j < n;j++) col[j] = col[j + 1];
col[n] = temp;
for(j = 1;j <= q;j++) if(XX[j] >= XX[i]) a[XX[j]][m] = col[X[j]];
}
}
return 0;
}
70分:在原来的基础上考虑20分的 n = 1 n=1 n=1 的情况,这时候就是从中间拿出元素放到最后,可以考虑用线段树,初始建立一棵1~(m+q)的树,每一个点记一下这段区间有多少个数,叶节点记一下这个点存的是哪一个数。每次先线段树上二分找对应的数并且输出,记录一下叶节点编号(注意这地方叶节点编号和这个点存的是什么数不是一回事)然后进行单点修改就行了。
一定要注意这两段的数组大小是不一样的!
代码如下:
#include
#include
#include
using namespace std;
#define mid (l + r >> 1)
const int N = 5e4 + 1;
const int M = 501;
const int N1 = 3e5 + 1;
int n,m,pos;
long long a[M][N],X[N1],Y[N1],w;
struct yjx{
int tre[N1 << 3];
long long num[N1 << 3];
void build(int k,int l,int r){
if(l == r){
if(l <= m){
tre[k] = 1;
num[k] = (long long)l;
}
return;
}
build(k << 1,l,mid);
build(k << 1 | 1,mid + 1,r);
tre[k] = tre[k << 1] + tre[k << 1 | 1];
}
void modify(int k,int l,int r,int x,int c){
if(l == r){
tre[k] = c;
if(c) num[k] = w;
return;
}
if(x <= mid) modify(k << 1,l,mid,x,c);
else modify(k << 1 | 1,mid + 1,r,x,c);
tre[k] = tre[k << 1] + tre[k << 1 | 1];
}
long long query(int k,int l,int r,int x){
//if(i == 1) printf("%d %d %d\n",l,r,tre[k]);
if(l == r){
pos = l;
return num[k];
}
if(tre[k << 1] >= x) return query(k << 1,l,mid,x);
//左区间的数字个数多于询问,说明目标在左区间,否则在右区间
else return query(k << 1 | 1,mid + 1,r,x - tre[k << 1]);
}
}str;
int main(){
int n1,q,i,j,x,y;
long long temp;
scanf("%d %d %d",&n,&m,&q);
for(i = 1;i <= q;i++){
scanf("%lld %lld",&X[i],&Y[i]);
}
str.build(1,1,m + q);
for(i = 1;i <= q;i++){
w = str.query(1,1,m + q,Y[i]);
printf("%lld\n",w);
str.modify(1,1,m + q,pos,0);
str.modify(1,1,m + q,m + i,1);
}
return 0;
}
正解:这里我们是从70分得到的启发,每一次操作实际就是修改一行再修改最后一列,那么只需要用 ( n + 1 ) (n+1) (n+1)棵线段树分别维护每一行和最后一列的信息就行了。为了节约空间,一是要动态开点,二是线段树只需要开到 m m m ,询问放到队尾的点全部存到vector里面就行了。另外为了节约建树,这里的tre的定义和上面相反,记的是一段区间被取出了多少个数。
代码如下:
#include
#include
#include
#include
using namespace std;
#define int long long
#define mid (l + r >> 1)
const int N = 1e6 + 2;
int n,m,p,q;
struct yjx{
int tot,rt[N],tre[N << 2],ls[N << 2],rs[N << 2];
vector<long long> v[N];
int query(int k,int l,int r,int c){
if(l == r) return l;
int x = mid - l + 1 - tre[ls[k]];
if(c <= x) return query(ls[k],l,mid,c);
else return query(rs[k],mid + 1,r,c - x);
//区别不大
}
void update(int &k,int l,int r,int x){
if(!k) k = ++tot;
++tre[k];
if(l == r) return;
if(x <= mid) update(ls[k],l,mid,x);
else update(rs[k],mid + 1,r,x);
//动态开点
}
long long modify1(long long x,long long y){
int pos = query(rt[n + 1],1,p,x);
long long ret;
update(rt[n + 1],1,p,pos);
if(pos <= n) ret = pos * m;
else ret = v[n + 1][pos - n - 1];
if(y) v[n + 1].push_back(y);
else v[n + 1].push_back(ret);
//分类,判断一下应该放进去哪一种答案
return ret;
//询问最后一列
}
long long modify2(long long x,long long y){
int pos = query(rt[x],1,p,y);
long long ret;
update(rt[x],1,p,pos);
if(pos < m) ret = (x - 1) * m + pos;
else ret = v[x][pos - m];
v[x].push_back(modify1(x,ret));
return ret;
//询问行
}
}str;
signed main(){
int i,x,y;
scanf("%lld %lld %lld",&n,&m,&q);
p = max(n,m) + q;
for(i = 1;i <= q;i++){
scanf("%lld %lld",&x,&y);
if(y == m) printf("%lld\n",str.modify1(x,0));
else printf("%lld\n",str.modify2(x,y));
}
return 0;
}
对于我来说括号序列问题没有一个简单的。
这题首先比较好像的是用一个栈维护括号序列,动态更新栈,看数据范围需要在这个过程中递推均摊 O ( 1 ) O(1) O(1) 求合法子串个数。
一个比较套路的思路是,括号只有匹配的时候产生贡献,所以假设有这么一个括号序列()(
,现在有1个合法子串,匹配一个右括号之后变为3个,发现这对新增的括号产生的贡献就是能接上左括号的合法子串个数再加上这一对括号,对于()(()
这种,补上一个左括号也是加一对的贡献。据此,可以设计一个 f i f_i fi 表示以 i i i 结尾的合法子串个数,某一段的合法子串个数就是这个递推数组的前缀和,栈内存左括号的下标。
回溯的时候判断是入栈还是出栈只需要打个标记就行了。注意不入栈不等于需要出栈,千万不要弹空栈。
代码如下:
#include
#include
#include
using namespace std;
const int N = 1e6 + 1;
struct yjx{
int nxt,to;
}e[N];
char s[N];
int n,ecnt = -1,head[N],f[N],fa[N],stak[N],top;
long long sum[N];
void save(int x,int y){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
head[x] = ecnt;
}
void dfs(int now){
int i,temp,x;
bool flag = 0;
if(s[now] == '('){
stak[++top] = now;
}
else if(top){
x = stak[top];
f[now] = f[fa[x]] + 1;
--top;
flag = 1;
}
sum[now] = sum[fa[now]] + f[now];
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa[now]) continue;
dfs(temp);
}
if(flag) stak[++top] = x;
else if(top) --top;
}
int main(){
int i,x;
long long res = 0;
memset(head,-1,sizeof(head));
scanf("%d",&n);
scanf("%s",s + 1);
for(i = 2;i <= n;i++){
scanf("%d",&x);
save(x,i);
fa[i] = x;
}
dfs(1);
for(i = 1;i <= n;i++){
res ^= (1ll * i * sum[i]);
}
printf("%lld\n",res);
return 0;
}
看数据范围,84分需要设计一个 O ( n 3 m ) O(n^3m) O(n3m) 的算法,所以先考虑这个。
提取一下这题的信息,就是从矩阵中取一些元素,要求至少取一个,每行至多取一个,每列取的元素至多占总共取的元素的一半。一个性质是最多只可能有一列取占超过一半的元素,加上性质1,这道题用补集思想做更简单一些,枚举哪一列取了超过一半的元素,然后进行三维递推,设 f ( i , j , k ) f(i,j,k) f(i,j,k) 表示考虑到第 i i i 行,在 l l l 列取了 j j j 个元素,其余的列取了 k k k 个元素的方案数。要注意这题的元素本身就是方案数,所以其余的列里面取的全部方案数就是 s u m i − a i , l sum_i-a_{i,l} sumi−ai,l ,
因此 f ( i , j , k ) = f ( i − 1 , j , k ) + f ( i − 1 , j − 1 , k ) × a i , l + f ( i − 1 , j , k − 1 ) × ( s u m i − a i , l ) f(i,j,k)=f(i-1,j,k)+f(i-1,j-1,k)\times a_{i,l}+f(i-1,j,k-1)\times (sum_i-a_{i,l}) f(i,j,k)=f(i−1,j,k)+f(i−1,j−1,k)×ai,l+f(i−1,j,k−1)×(sumi−ai,l) ,最终的总非法方案数就是 ∑ j > k f ( n , j , k ) . \sum\limits_{j>k}f(n,j,k). j>k∑f(n,j,k).
求总方案数跟上面也差不多。
现在考虑怎么优化成 O ( n 2 m ) O(n^2m) O(n2m) ,发现一个问题就是枚举 j , k j,k j,k ,因为这两个其实到最后唯一的意义就是比较一下大小,所以考虑把这个合并成一维,表示第 l l l 列和剩下所有列选的元素的个数之差,那么 f ( i , j ) = f ( i − 1 , j ) + f ( i − 1 , j − 1 ) × a i , l + f ( i − 1 , j + 1 ) × ( s u m i − a i , l ) f(i,j)=f(i-1,j)+f(i-1,j-1)\times a_{i,l}+f(i-1,j+1)\times (sum_i-a_{i,l}) f(i,j)=f(i−1,j)+f(i−1,j−1)×ai,l+f(i−1,j+1)×(sumi−ai,l) ,最终的总非法方案数就是 ∑ j > 0 f ( n , j ) . \sum\limits_{j>0}f(n,j). j>0∑f(n,j).显然下标不能为负,所以整体上移 n n n ,当然还要注意把数组开大两倍。
注意不能忘记限制1,不管是非法的方案还是总方案都要包括一种不选的方案数,否则很容易发生debug非常久最后发现总方案数求错的尴尬事件 ,别问我为什么知道 。
代码如下:
#include
#include
#include
using namespace std;
const int mod = 998244353;
long long f[101][4001],g[101][2001],sum[101],a[101][2001];
int main(){
int i,j,k,n,m;
long long res = 0,c = 0;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
for(j = 1;j <= m;j++){
scanf("%lld",&a[i][j]);
sum[i] = (sum[i] + a[i][j]) % mod;
}
}
g[0][0] = 1;
for(i = 1;i <= n;i++){
g[i][0] = 1;
for(j = 1;j <= i;j++){
g[i][j] = (g[i - 1][j - 1] * sum[i] % mod + g[i - 1][j]) % mod;
}
}
for(i = 1;i <= n;i++) c = (c + g[n][i]) % mod;
for(k = 1;k <= m;k++){
memset(f,0,sizeof(f));
f[0][n] = 1;
for(i = 1;i <= n;i++){
for(j = n - i;j <= n + i;j++){
f[i][j] = (f[i - 1][j - 1] * a[i][k] % mod + f[i - 1][j + 1] * (sum[i] - a[i][k] + mod) % mod + f[i - 1][j]) % mod;
}
}
for(i = 1;i <= n;i++){
res = (res + f[n][n + i]) % mod;
}
}
printf("%lld\n",(c - res + mod) % mod);
return 0;
}
他改变了OI
这道题元素非常多,必须一一分析。
首先,按照时间的计算,儒略历可以4年为一段,格里高利历400年为一段,由于是以1582年10月4号为分界,为了便于计算,公元前为一段,公元1年~ 1582年10月4日为一段,1582年10月15日~ 1582年12月31日为一段,1583年1月1日~1599年12月31日为一段,1600年1月1日之后为一段。经过打表可以算出来这几个分界点的儒略日(这个说实话很容易打错,千万别打错了)。
首先把所有的儒略日分好段,剩下的就是算它是从起点开始数的第几天。以公元前为例,首先已知4年一个循环,所以可以直接%4,顺便算出来应该从哪一年的1月1日开始数。由于4年短,所以可以直接枚举应该是哪一年,去掉对应的年数,剩下的就是枚举应该在哪一个月(注意枚举月只需要枚举到11,因为大于说明日期应该轮到下一个月)。考虑到%4之后应该从1号开始算,所以最终日期要+1.
这道题全程都是算段时间,说实话确实非常容易算错,每一段最好都用这段的第一天或者第二个月第一天验证一下。另外这题大样例非常非常水,绝大多数的日期都是1600年之后的,所以为了验证别的段是否正确确实得好好寻找一下。
另外这题我由于思路原因实现比较长(130多行,但是大部分基本是重复的),时间常数大,但是思路还是比较清晰的,能过,不慢。
最后这题别忘了开long long.
总之此题从打表到算时间,全程都要求极高的准确度,而且要求实现非常合理,失误代价极大,确实是联赛以来最难的一道纯模拟(我不知道凭什么这道题是个绿题)。
代码如下:
#include
#include
#include
using namespace std;
#define int long long
int common[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
int leap[13] = {0,31,29,31,30,31,30,31,31,30,31,30,31};
//预处理一个表
signed main(){
bool flag;
int i,j,n,x,y,m,d,t,q;
scanf("%lld",&q);
for(i = 1;i <= q;i++){
scanf("%lld",&t);
if(t <= 1721423){//公元前
flag = 0;
y = t / 1461 * 4,t %= 1461;
if(t >= 366 + 365 + 365) y += 3,t -= 366 + 365 + 365,flag = 1;
else if(t >= 366 + 365) y += 2,t -= 366 + 365,flag = 1;
else if(t >= 366) y++,t -= 366,flag = 1;
//暴力判断应该是这个循环中的哪一年
if(flag){
for(j = 1;j <= 11;j++){
if(t >= common[j]) t -= common[j];
else break;
}
m = j;
d = t + 1;
}
else{
for(j = 1;j <= 11;j++){
if(t >= leap[j]) t -= leap[j];
else break;
}
m = j;
d = t + 1;
}
//暴力判断应该是哪一个月
printf("%lld %lld %lld BC\n",d,m,4713 - y);
}
else if(t <= 2299160){//公元后~1582年10月4日,操作同前
flag = 0;
t -= 1721424;
y = t / 1461 * 4,t %= 1461;
if(t >= 365 + 365 + 365) y += 3,t -= 365 + 365 + 365,flag = 0;
else if(t >= 365 + 365) y += 2,t -= 365 + 365,flag = 1;
else if(t >= 365) y++,t -= 365,flag = 1;
else flag = 1;
if(flag){
for(j = 1;j <= 11;j++){
if(t >= common[j]) t -= common[j];
else break;
}
m = j;
d = t + 1;
}
else{
for(j = 1;j <= 11;j++){
if(t >= leap[j]) t -= leap[j];
else break;
}
m = j;
d = t + 1;
}
printf("%lld %lld %lld\n",d,m,y + 1);
}
else if(t <= 2299238){//1582年10月15日~12月31日
t -= 2299161;
y = 1582;
t += 15;//先加上15天,相当于从9月31号开始算
for(j = 10;j <= 11;j++){
if(t >= common[j]) t -= common[j];
else break;
}
m = j;
d = t;
printf("%lld %lld %lld\n",d,m,y);
}
else if(t <= 2305447){//1583年~1599年
t -= 2299239;
y = 1583;
for(j = 1583;j <= 1599;j++){
if(j % 4 == 0 && j % 100 != 0 || j % 400 == 0) x = 366;
else x = 365;
if(t >= x) t -= x,++y;
else break;
}
//直接枚举年份
if(j % 4 == 0 && j % 100 != 0 || j % 400 == 0) flag = 0;
else flag = 1;
if(flag){
for(j = 1;j <= 11;j++){
if(t >= common[j]) t -= common[j];
else break;
}
m = j;
d = t + 1;
}
else{
for(j = 1;j <= 11;j++){
if(t >= leap[j]) t -= leap[j];
else break;
}
m = j;
d = t + 1;
}
printf("%lld %lld %lld\n",d,m,y);
}
else{//1600年之后,400年一循环
flag = 0;
t -= 2305448;
y = 1600 + t / 146097 * 400,t %= 146097;
for(j = 0;j <= 399;j++){
if(j % 4 == 0 && j % 100 != 0 || j % 400 == 0) x = 366;
else x = 365;
if(t >= x) t -= x,++y;
else break;
}
//暴力枚举是400年循环中的哪一年
if(j % 4 == 0 && j % 100 != 0 || j % 400 == 0) flag = 0;
else flag = 1;
if(flag){
for(j = 1;j <= 11;j++){
if(t >= common[j]) t -= common[j];
else break;
}
m = j;
d = t + 1;
}
else{
for(j = 1;j <= 11;j++){
if(t >= leap[j]) t -= leap[j];
else break;
}
m = j;
d = t + 1;
}
printf("%lld %lld %lld\n",d,m,y);
}
}
return 0;
}
这道题不存在调用死循环 (人家写代码不会犯这种丢人的错误,不像我) ,所以调用关系应该构成一个DAG。
经过画图和推导,发现首先为了方便应该计算一下每一个加法应该被乘了多少,后面的乘法对前面的产生贡献,所以可以倒推求每一个加法当中乘的次数。这样做就能解决没有操作3的情况。
除了计算每一个乘多少,还有一个关键的问题是要知道这个函数被调用多少次,这个就需要顺推,推的过程中顺便要算上乘的次数,根据上面的结论,我们在算调用次数(乘法其实也可以当作多次调用)的时候也是反向计算的,用链前存图直接遍历就行了。最后只需要取出所有的操作1进行修改就行了。
总之此题如果想要计算清晰,一种很好的思路就是分别讨论没有操作2和没有操作3的情况并最终合到一起,需要很好的思路。挺有意思的一道题。
代码如下:
#include
#include
#include
#include
using namespace std;
const int N = 2e5 + 1;
const int M = 2e6 + 1;
const int mod = 998244353;
struct yjx{
int nxt,to;
}e1[M],e2[M];
queue<int> Q;
int n,m,q,ecnt1 = -1,ecnt2 = -1,head1[N],head2[N],in1[N],in2[N],op[N],id[N];
long long mul[N],cnt[N],c[N],ad[N];
void save1(int x,int y){
e1[++ecnt1].nxt = head1[x];
e1[ecnt1].to = y;
head1[x] = ecnt1;
++in1[y];
}
void save2(int x,int y){
e2[++ecnt2].nxt = head2[x];
e2[ecnt2].to = y;
head2[x] = ecnt2;
++in2[y];
}
void topo(){
int i,now,temp;
mul[0] = 1;
for(i = 0;i <= q;i++){
if(!in2[i]) Q.push(i);
}
while(!Q.empty()){
now = Q.front();
Q.pop();
for(i = head2[now];~i;i = e2[i].nxt){
temp = e2[i].to;
--in2[temp];
if(!in2[temp]) Q.push(temp);
mul[temp] = mul[temp] * mul[now] % mod;
//printf("%d %lld\n",temp,mul[temp]);
}
}
}
void solve(){
int i,now,temp;
long long sum;
cnt[0] = 1;
for(i = 0;i <= m;i++){
if(!in1[i]) Q.push(i);
}
while(!Q.empty()){
now = Q.front();
Q.pop();
sum = 1;
for(i = head1[now];~i;i = e1[i].nxt){
temp = e1[i].to;
--in1[temp];
if(!in1[temp]) Q.push(temp);
cnt[temp] = (cnt[temp] + cnt[now] * sum % mod) % mod;
sum = sum * mul[temp] % mod;
}
}
}
int main(){
int i,j,x,y;
scanf("%d",&n);
memset(head1,-1,sizeof(head1));
memset(head2,-1,sizeof(head2));
for(i = 1;i <= n;i++) scanf("%lld",&c[i]);
scanf("%d",&q);
for(i = 1;i <= q;i++){
scanf("%d",&op[i]);
if(op[i] == 1){
scanf("%d %d",&id[i],&ad[i]);
mul[i] = 1;
}
if(op[i] == 2){
scanf("%d",&x);
mul[i] = x;
}
if(op[i] == 3){
mul[i] = 1;
scanf("%d",&x);
for(j = 1;j <= x;j++){
scanf("%d",&y);
save1(i,y),save2(y,i);
}
}
}
scanf("%d",&m);
for(i = 1;i <= m;i++){
scanf("%d",&x);
save1(0,x),save2(x,0);
}
topo();
solve();
for(i = 1;i <= n;i++) c[i] = c[i] * mul[0] % mod;
for(i = 1;i <= q;i++){
if(op[i] == 1) c[id[i]] = (c[id[i]] + ad[i] * cnt[i] % mod) % mod;
}
for(i = 1;i <= n;i++) printf("%lld ",c[i]);
puts("");
return 0;
}
这道题个人认为基本上没有暴力。
作为一个Hanoist,这种题的一般思路我们非常熟悉了,通常是逐步缩小一个大规模的问题,最终化为同一形式的子问题。
现在就先考虑一下最小的问题,也就是n=2的情况下怎么解。
经过一番实验,发现一种很好的方法:假设有A和B两堆球和一个空堆C,两种球是0和1,A中一共有s个0.先从B中拿出去s个球放到C,然后搬空堆A,0放到B当中,1放到C当中,再把堆B顶端的0放回A,堆C顶端的1放回A。现在把B堆的球全都放进C,把A堆顶端的1放进B,此时对C重复上面的过程,0放到A,1放到B,任务完成。
上述操作的步数是s+m+m+(m-s)+(m-s)+m=(5m-s)步。
这个操作全程只需要这两个堆自己和空堆配合,所以是一种普遍的处理方式;如果有多种球,最好是可以把所有的堆都分成两两一组,分别操作解决,这个分组的过程感觉非常像一个二叉树。
因此我们可以由此得到启发,假设现在处理的堆是 [ l , r ] [l,r] [l,r] ,其中也只有对应颜色的球,那么我们可以以 m i d mid mid 为分界线,不大于 m i d mid mid 的看作0,这一来把两端的堆两两匹配,就能使得一侧全为0而另一侧全为1,于是就可以接着向下递归求解。另外考虑到n>2的时候任意两堆0和1的个数不一定相等,如果这一对0多于m个,应该把左边的全部复原为0,否则把右边的复原为1.
假设操作次数为 T T T,那么应该有 T ( n ) = 2 T ( n 2 ) × n ( 5 m − s ) = 5 m n l o g n T(n)=2T(\frac{n}{2})\times n(5m-s)=5mnlogn T(n)=2T(2n)×n(5m−s)=5mnlogn,可以通过此题。
总之这道题确实满满的汉诺塔既视感,事实上也确实是汉诺塔问题的内核,总之只要发现了分治基本上就解决了,如果没有从n=2开始那么很容易最终卡在最后n=2的时候,然后前功尽弃(鬼知道我经历了什么),是一道很有意思的思维题。
不得不说20年的初赛和联赛没有一道不是思维题,属于是历史思维含量最高的一年了,而且确实大幅高于其他年份/捂脸
代码如下:
#include
#include
#include
using namespace std;
#define mid (l + r >> 1)
const int N = 52;
const int M = 402;
const int S = 820001;
int n,m,tot,stak[N][M],step[S][2],top[N];
bool vis[N];
void op(int x,int y){//一定要把这写成函数,否则实现会写的非常恶心
step[++tot][0] = x,step[tot][1] = y;
stak[y][++top[y]] = stak[x][top[x]--];
}
void solve(int l,int r){
if(l == r) return;
int i,j,k;
memset(vis,0,sizeof(vis));
for(i = l;i <= mid;i++){
for(j = mid + 1;j <= r;j++){
if(vis[i] || vis[j]) continue;
int s = 0;
for(k = 1;k <= m;k++) s += (stak[i][k] <= mid) + (stak[j][k] <= mid);
if(s >= m){
s = 0;
for(k = 1;k <= m;k++) s += (stak[i][k] <= mid);
for(k = 1;k <= s;k++) op(j,n + 1);
while(top[i]){
if(stak[i][top[i]] <= mid) op(i,j);
else op(i,n + 1);
}
for(k = 1;k <= s;k++) op(j,i);
for(k = 1;k <= m - s;k++) op(n + 1,i);
for(k = 1;k <= m - s;k++) op(j,n + 1);
for(k = 1;k <= m - s;k++) op(i,j);
while(top[n + 1]){
if(top[i] == m || stak[n + 1][top[n + 1]] > mid) op(n + 1,j);
else op(n + 1,i);
}
vis[i] = 1;
}
else{
s = 0;
for(k = 1;k <= m;k++) s += (stak[j][k] > mid);
for(k = 1;k <= s;k++) op(i,n + 1);
while(top[j]){
if(stak[j][top[j]] > mid) op(j,i);
else op(j,n + 1);
}
for(k = 1;k <= s;k++) op(i,j);
for(k = 1;k <= m - s;k++) op(n + 1,j);
for(k = 1;k <= m - s;k++) op(i,n + 1);
for(k = 1;k <= m - s;k++) op(j,i);
while(top[n + 1]){
if(top[j] == m || stak[n + 1][top[n + 1]] <= mid) op(n + 1,i);
else op(n + 1,j);
}
vis[j] = 1;
}
}
}
solve(l,mid);
solve(mid + 1,r);
}
int main(){
int i,j,x;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
for(j = 1;j <= m;j++){
scanf("%d",&stak[i][j]);
}
top[i] = m;
}
solve(1,n);
printf("%d\n",tot);
for(i = 1;i <= tot;i++){
printf("%d %d\n",step[i][0],step[i][1]);
}
return 0;
}
这部分就不讲究怎么把一道题做出来了,重点在于怎么高效的面对时间复杂度设计程序并且高效的得分。
这道题只有一个空格,所以解题当中一个绝对正确的部分就是考虑空格的移动而不是棋的移动,现在问题就是这个指定块怎么移动到目标格子上,考虑用bfs进行模拟,显然每一个状态是四维的,那么这个复杂度大约能过60分。
考虑怎么模拟,空格的移动相当于交换,枚举空格的移动,如果空格移动到的位置是指定块的位置,就应该交换指定块和空格的位置,直到指定块到位。队列当中的步数是单调的,所以算法成立。
实现起来也很简单,事实上这种算法开了O2能得到80分。
代码如下:
#include
#include
#include
#include
#include
using namespace std;
inline int read(){
int x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x * f;
}
const int N = 31;
typedef pair<int,pair<int,pair<int,pair<int,int> > > > pr;
queue<pr> Q;
int n,m,G[N][N];
int dx[4] = {0,1,0,-1};
int dy[4] = {1,0,-1,0};
bool vis[N][N][N][N];
int bfs(int bx,int by,int sx,int sy,int ex,int ey){
int x,y,a,b,s;
while(!Q.empty()) Q.pop();
Q.push(make_pair(bx,make_pair(by,make_pair(sx,make_pair(sy,0)))));
while(!Q.empty()){
x = Q.front().first,y = Q.front().second.first;
a = Q.front().second.second.first,b = Q.front().second.second.second.first;
s = Q.front().second.second.second.second;
Q.pop();
if(vis[x][y][a][b]) continue;
vis[x][y][a][b] = 1;
if(a == ex && b == ey){
return s;
}
int i;
for(i = 0;i < 4;i++){
int xx = x + dx[i],yy = y + dy[i];
int aa = a,bb = b;
if(xx == aa && yy == bb) aa = x,bb = y;
if(xx < 1 || xx > n || yy < 1 || yy > m || !G[xx][yy] || vis[xx][yy][aa][bb]) continue;
Q.push(make_pair(xx,make_pair(yy,make_pair(aa,make_pair(bb,s + 1)))));
}
}
return -1;
}
int main(){
int i,j,q,sx,sy,bx,by,ex,ey;
n = read(),m = read(),q = read();
for(i = 1;i <= n;i++){
for(j = 1;j <= m;j++){
G[i][j] = read();
}
}
for(i = 1;i <= q;i++){
bx = read(),by = read(),sx = read(),sy = read(),ex = read(),ey = read();
if(sx == ex && sy == ey) puts("0");
else{
memset(vis,0,sizeof(vis));
printf("%d\n",bfs(bx,by,sx,sy,ex,ey));
}
}
return 0;
}
这题明显的一个状压,首先尝试设计,设 f i f_i fi 表示猪的状态为 i i i (0为存活)时需要的最少的鸟的数目,那么显然有 f i ∣ j = m i n { f i ∣ j , f i + f j } f_{i|j}=min\{f_{i|j},f_i+f_j\} fi∣j=min{fi∣j,fi+fj},而初始状态下 f 0 = 0 f_0=0 f0=0,剩下的就需要我们求解了。
现在由于已经有原点,所以每两个点可以确定唯一的一条抛物线,先枚举两点计算出二次函数的一般式(这个可以手推,需要进行一波玄学调精度)并且记录一下这个状态,设为 s s s ,然后枚举剩下的点看是否在这条抛物线上,如果在就加入到这个状态当中,最终 f s = 1. f_s=1. fs=1.虽然同一条抛物线可能被多次计入,但是相同的抛物线的状态最终属于同一个集合,所以可以在dp的时候优化掉。
最后枚举状态的时候,特判一下如果 j j j 是 i i i 的子集就不更新,所以实际上时间复杂度会稍低于 O ( 4 n ) O(4^n) O(4n) ,精度调好的话能得70分。
此题的正解相比之下代码难度并没有什么提高,但是需要用到一个贪心,我个人觉得不是很容易想,不过即使想到了只需要在这个的基础上稍微改一改就足够了,这个暴力是一个很优秀的写法。
代码如下:
#include
#include
#include
#include
#include
using namespace std;
const int N = 1 << 18;
const double eps = 1e-9;
map<double,map<double,bool> > vis;
long long f[N];
double X[19],Y[19],X2[19];
int main(){
int i,j,k,n,m,tot,t,st;
double a,b;
scanf("%d",&t);
while(t--){
vis.clear();
memset(f,0x3f,sizeof(f));
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
scanf("%lf %lf",&X[i],&Y[i]);
X2[i] = X[i] * X[i];
}
for(i = 1;i <= n;i++){
f[(1 << (i - 1))] = 1;
}
for(i = 1;i <= n;i++){
for(j = i + 1;j <= n;j++){
b = (X2[i] * Y[j] - X2[j] * Y[i]) / (X2[i] * X[j] - X2[j] * X[i]);
a = (Y[i] - X[i] * b) / X2[i];
//一定要选择一种尽可能少出现除法的形式
if(a >= -eps) continue;
st = (1 << (i - 1)) + (1 << (j - 1));
for(k = 1;k <= n;k++){
if(k - i < eps || k - j < eps) continue;
if(fabs(a * X[k] * X[k] + b * X[k] - Y[k]) <= eps) st += (1 << (k - 1));
}
f[st] = 1;
}
}
f[0] = 0;
for(i = 0;i < (1 << n);i++){
if(f[i] > 1e9) continue;
for(j = 1;j < i;j++){
if(f[j] > 1e9 || j & i == j) continue;
f[i | j] = min(f[i | j],f[i] + f[j]);
}
}
printf("%lld\n",f[(1 << n) - 1]);
}
return 0;
}
这题可以直接按题意模拟,利用堆来维护信息。
只不过这个按题意模拟也不是那么简单的一件事情,首先,堆内的元素不能动态修改,因此这个每秒钟增长只能先一直记录,等到取出的时候再全部加上,分成两段后还要全部去掉(以防止精度误差)。由于这期间实际上并没有什么元素出堆,而且考虑到这题不关心入堆的时间,所以最后直接取倍数就可以了。
总之这道题按题意模拟不难写,但是题干比较长,思考的时候容易自己想的过于复杂。
这道题这样写就可以得到90,正解需要观察出这题的一个隐含的单调性的性质:先切一定比后切更长,所以最终用三个队列,分别维护未切的、切完之后大的和小的两段,这三个队列自带单调性,没有log。其实正解也不难想,但是这道题确实先写这个90分暴力,然后优化成100更容易一些。
(90分)代码如下:
#include
#include
#include
#include
#include
using namespace std;
inline int read_int(){
int x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x * f;
}
priority_queue<int> Q;
int main(){
int i,j,n,m,q,u,v,t,x,y,w,add = 0;
double p;
n = read_int(),m = read_int(),q = read_int(),u = read_int(),v = read_int(),t = read_int();
p = 1.0 * u / v;
for(i = 1;i <= n;i++){
x = read_int();
Q.push(x);
}
for(i = 1;i <= m;i++){
w = Q.top() + add;
Q.pop();
x = floor(1.0 * w * p),y = w - x;
x -= (add + q),y -= (add + q);
Q.push(x),Q.push(y);
if(i % t == 0) printf("%d ",w);
add += q;
}
puts("");
for(i = 1;!Q.empty();i++){
if(i % t == 0) printf("%d ",Q.top() + add);
Q.pop();
}
puts("");
return 0;
}
一道不太好做的图论。
首先是k=0,这个就是一个普通的最短路计数,写的熟练基本不用动脑子就能白拿30分。(然而我显然不是熟练的那一批,笑)
对于70分是不存在0边的,相当于这个图上没有可能无限走的环,所以可以试着进行dp。设 f i , j f_{i,j} fi,j 表示到第 i i i 个点,路径长度比最短路多 j j j 的方案数,最短路设为 d i s dis dis ,那么如果有 u − > v u->v u−>v ,要从 f u , c f_{u,c} fu,c 转移到 f j , d f_{j,d} fj,d ,两点间的距离是 w w w ,那么就应该有 d = c + ( d i s u + w − d i s v ) d=c+(dis_u+w-dis_v) d=c+(disu+w−disv) , c c c 可以枚举,这一来转移式就出来了。另外考虑到应该先更新离点1近的点 (我理解是因为加入进来的方案数少,便于后续的转移),所以需要先排一个序。总之这题不是一个一般的图论,仅仅是借用图的形态,不要把这个跟拓扑dp以及什么最短路搞混了。这题写起来总之确实是体验极差就是了。
70分代码如下:
#include
#include
#include
#include
using namespace std;
#define int long long
const int N = 2e5 + 1;
struct yjx{
int nxt,to,c;
}e[N];
queue<int> Q;
int n,m,k,mod,ecnt = -1,head[N],dis[N],id[N];
long long f[N][51],res;
bool vis[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;
}
bool cmp(int x,int y){
return dis[x] < dis[y];
}
void SPFA(){//求最短路
int i,now,temp;
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[1] = 0;
Q.push(1);
while(!Q.empty()){
now = Q.front();
Q.pop();
vis[now] = 0;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(dis[temp] > dis[now] + e[i].c){
dis[temp] = dis[now] + e[i].c;
if(!vis[temp]){
Q.push(temp);
vis[temp] = 1;
}
}
}
}
}
void dp(){
int i,j,l,now,temp,d;
memset(f,0,sizeof(f));
f[1][0] = 1;
for(i = 1;i <= n;i++) id[i] = i;
sort(id + 1,id + n + 1,cmp);
for(l = 0;l <= k;l++){
for(i = 1;i <= n;i++){
now = id[i];
for(j = head[now];~j;j = e[j].nxt){
temp = e[j].to;
d = l + (dis[now] + e[j].c - dis[temp]);
if(d <= k) f[temp][d] = (f[temp][d] + f[now][l]) % mod;
}
}
}
for(i = 0;i <= k;i++){
res = (res + f[n][i]) % mod;
}
}
signed main(){
int i,t,x,y,w;
scanf("%lld",&t);
while(t--){
res = 0;
memset(head,-1,sizeof(head));
ecnt = -1;
scanf("%lld %lld %lld %lld",&n,&m,&k,&mod);
for(i = 1;i <= m;i++){
scanf("%lld %lld %lld",&x,&y,&w);
save(x,y,w);
}
SPFA();
dp();
printf("%lld\n",res);
}
return 0;
}
一道分类讨论部分分题。
一共有四种特殊限制,其中最后一个分支数基本没用(比较容易判断出来这个应该是用来给实现不是很好的正解用的),这题并没有非特殊限制的部分分,所以问题就在于研究这几个不同的情况分别是怎么解的。一共可以得到55分。
m=1,只选一条赛道,那就是求树的直径的模板。
a i = 1 a_i=1 ai=1,菊花图,选出的赛道应该是两两匹配,由于希望最小值最大,那么这就是个很经典的贪心了,直接对所有的边按权排序,首尾相加,取第 m m m 对就是答案。
b i = a i + 1 b_i=a_i+1 bi=ai+1 ,树的形态是一条链,经典的二分答案+验证。
这三个部分都不难写,唯一需要注意的也就是数组的大小是否有区别了。这题的数组好在没有多维的,全都拉满就行了。
另外,感谢此题没有多测。
代码如下:
#include
#include
#include
using namespace std;
const int N = 5e4 + 1;
bool sub1,sub2;
struct yjx{
int nxt,to;
long long c;
}e[N << 1];
struct wyx{
int x,y;
long long w;
}a[N];
bool cmp(wyx p,wyx q){
return p.x < q.x;
}
bool cmpp(wyx p,wyx q){
return p.w > q.w;
}
int n,m,ecnt = -1,head[N];
int hson[N];
long long f[N],f2[N],g[N];
void save(int x,int y,long long w){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
e[ecnt].c = w;
head[x] = ecnt;
}
void dfs1(int now,int fa){
int i,temp;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
dfs1(temp,now);
if(f[temp] + e[i].c > f[now]) f2[now] = f[now],f[now] = max(f[now],f[temp] + e[i].c),hson[now] = temp;
else if(f[temp] + e[i].c > f2[now]) f2[now] = f[temp] + e[i].c;
}
}
void dfs2(int now,int fa){
int i,temp;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
if(temp == hson[now]) g[temp] = max(g[now],f2[now]) + e[i].c;
else g[temp] = max(g[now],f[now]) + e[i].c;
dfs2(temp,now);
}
}
bool check(long long mid){
int i,cnt = 0;
long long temp = 0;
for(i = 1;i < n;i++){
temp += a[i].w;
if(temp >= mid) temp = 0,++cnt;
}
return cnt >= m;
}
int main(){
int i;
long long l,r,mid;
long long res;
sub1 = 1,sub2 = 1;
scanf("%d %d",&n,&m);
memset(head,-1,sizeof(head));
for(i = 1;i < n;i++){
scanf("%d %d %lld",&a[i].x,&a[i].y,&a[i].w);
save(a[i].x,a[i].y,a[i].w),save(a[i].y,a[i].x,a[i].w);
if(a[i].y != a[i].x + 1) sub1 = 0;
if(a[i].x ^ 1) sub2 = 0;
}
if(m == 1){
res = 0;
dfs1(1,0);
dfs2(1,0);
for(i = 1;i <= n;i++){
res = max(res,f[i] + g[i]);
}
printf("%lld\n",res);
}
else if(sub1){
sort(a + 1,a + n,cmp);
l = 1,r = 1e12;
while(l < r){
mid = (l + r + 1) >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
printf("%lld\n",l);
}
else if(sub2){
res = 1e12;
sort(a + 1,a + n,cmpp);
for(i = 1;i <= m;i++){
res = min(res,a[i].w + a[2 * m - i + 1].w);
}
printf("%lld\n",res);
}
return 0;
}
由于模拟用时原因,到这道题的时候没有去看特殊限制,直接写的高复杂度暴力,这里也就只介绍这个了。
这题首先给人感觉有点像著名树形dp模板题“没有上司的舞会”的反向版本。根据这个经验,考虑怎么设计 O ( q n ) O(qn) O(qn) dp。
设 f i , 0 / 1 f_{i,0/1} fi,0/1表示考虑到点 i i i ,不选/选这个点的最小花费。不选则所有儿子必须选,选的话则儿子可选可不选。
对于几个必须不选/选的点,这里就有一个常用的技巧:对于求最小花费的问题,如果强制要求一样物品选/不选,可以直接把它的花费设置为0/无限大,最后再一起加上。这样一来这道题就很容易可以得到44分了。
代码如下:
#include
#include
#include
using namespace std;
inline int read_int(){
int x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x * f;
}
inline long long read_long(){
long long x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x * f;
}
const int N = 1e5 + 1;
struct yjx{
int nxt,to;
}e[N << 1];
int ecnt = -1,head[N];
long long f[N][2],a[N];
void save(int x,int y){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
head[x] = ecnt;
}
void dfs(int now,int fa){
int i,temp;
f[now][1] = a[now];
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
dfs(temp,now);
f[now][0] += f[temp][1];
f[now][1] += min(f[temp][1],f[temp][0]);
}
}
int main(){
int i,n,q,x,y,c1,c2;
long long temp1,temp2,res;
char s[2];
n = read_int(),q = read_int();
scanf("%s",s);
memset(head,-1,sizeof(head));
for(i = 1;i <= n;i++) a[i] = read_long();
for(i = 1;i < n;i++){
x = read_int(),y = read_int();
save(x,y),save(y,x);
}
for(i = 1;i <= q;i++){
x = read_int(),c1 = read_int(),y = read_int(),c2 = read_int();
temp1 = a[x],temp2 = a[y];
if(!c1) a[x] = 1e12;
else a[x] = 0;
if(!c2) a[y] = 1e12;
else a[y] = 0;
memset(f,0,sizeof(f));
dfs(1,0);
res = min(f[1][1],f[1][0]);
if(c1) res += temp1;
if(c2) res += temp2;
if(res < 1e12) printf("%lld\n",res);
else puts("-1");
a[x] = temp1,a[y] = temp2;
}
return 0;
}
又是一个特殊限制分类讨论的,只不过这一次有高次暴力分了。
这题有三个限制,一个是可以 O ( n 2 ) O(n^2) O(n2) ,一个是链,一个是完美二叉树。
前者直接枚举边然后 O ( n ) O(n) O(n) 求重心;
对于链,如果长度是奇数就只有一个中点,长度是偶数就是两个中点,这个也不难处理,但是要时刻注意这道题的性质是链而不是 b i = a i + 1 b_i=a_i+1 bi=ai+1 ,所以要记一下排完之后的链每一个位置原来的数字是多少,不能把下标和值混为一谈,这个是非常容易被忽略的。
对于完美二叉树,经过一些观察,设一条边连接的两点是x和y,且x的深度大于y,可以得到以下几个结论:
A 不含根的那个子树的重心一定是x
B 含根的那个子树的重心一定是根节点另一侧的子节点
C 如果x是叶子,根节点也是重心(注意B和C是能同时成立的!)
因此只要先找到根(度数为2),然后把一侧子树标记为左子树,就可以枚举边 O ( 1 ) O(1) O(1) 处理了。
这道题的三个点都要注意不能把下标当值加进去,不得不说,没有那个明确的数值而只是一个性质,代码写起来会麻烦不少。
代码如下:
#include
#include
#include
using namespace std;
const int N = 2001;
const int M = 3e5 + 1;
struct yjx{
int nxt,to;
}e[M << 1];
int n,m,tot,ecnt = -1,head[M],siz[N],in[M],dep[M];
bool l[M];
long long num[M],res,x[M],y[M],rt,ls,rs,sum[M];
void save(int x,int y){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
head[x] = ecnt;
++in[y];
}
void solve(int now,int fa){
int i,temp;
dep[now] = dep[fa] + 1;
if(l[fa]) l[now] = 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
if(now == rt){
if(!ls) ls = temp,l[temp] = 1;
else rs = temp;
}
solve(temp,now);
}
}
void pre(int now,int fa){
int i,temp;
num[++tot] = now;
while(1){
i = head[now];
temp = e[i].to;
if(temp == fa){
i = e[i].nxt;
if(i == -1) break;
temp = e[i].to;
}
num[++tot] = temp;
fa = now,now = temp;
}
}
void count(int now,int fa){
int i,temp;
siz[now] = 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
count(temp,now);
siz[now] += siz[temp];
}
}
void dfs(int now,int fa){
int i,temp,sum = 0,hsiz = 0;
siz[now] = 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
dfs(temp,now);
siz[now] += siz[temp];
sum += siz[temp];
hsiz = max(hsiz,siz[temp]);
}
if(hsiz <= m / 2 && m - sum - 1 <= m / 2) res += now;
}
int main(){
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
int i,t;
scanf("%d",&t);
while(t--){
res = 0,ecnt = -1;
memset(head,-1,sizeof(head));
scanf("%d",&n);
if(n <= 2000){
for(i = 1;i < n;i++){
scanf("%lld %lld",&x[i],&y[i]);
save(x[i],y[i]),save(y[i],x[i]);
}
for(i = 1;i < n;i++){
count(x[i],y[i]);
m = siz[x[i]];
dfs(x[i],y[i]);
m = n - m;
dfs(y[i],x[i]);
}
printf("%lld\n",res);
}
else if(n <= 50000){
tot = 0;
memset(in,0,sizeof(in));
for(i = 1;i < n;i++){
scanf("%lld %lld",&x[i],&y[i]);
save(x[i],y[i]),save(y[i],x[i]);
}
for(i = 1;i <= n;i++){
if(in[i] == 1) break;
}
pre(i,0);
//printf("%d\n",tot);
for(i = 1;i < n;i++){
m = n - i;
if(i & 1) res += num[(i + 1) >> 1];
else res += num[(i + 1) >> 1] + num[(i + 2) >> 1];
if(m & 1) res += num[(n + i + 1) >> 1];
else res += num[(n + i + 1) >> 1] + num[(n + i + 2) >> 1];
//printf("%lld %d %d\n",res,(n + i + 1) >> 1,(i + 1) >> 1);
}
printf("%lld\n",res);
}
else if(n == 262143){
rt = 0,ls = 0,rs = 0;
memset(l,0,sizeof(l));
memset(in,0,sizeof(in));
for(i = 1;i < n;i++){
scanf("%lld %lld",&x[i],&y[i]);
save(x[i],y[i]),save(y[i],x[i]);
}
for(i = 1;i <= n;i++){
if(in[i] == 2){
rt = i;
break;
}
}
solve(rt,0);
for(i = 1;i < n;i++){
if(dep[x[i]] < dep[y[i]]) swap(x[i],y[i]);
if(dep[x[i]] == 18){
res += rt;
}
res += x[i];
if(l[x[i]]) res += rs;
else res += ls;
}
printf("%lld\n",res);
}
}
return 0;
}
贪心+dp没有简单题。
首先这题会给人非常强烈的二分答案/贪心既视感,前者也就是看看(毕竟要求和),但是后者是有可行性的,因为有结论 a 2 + b 2 ≤ ( a + b ) 2 a^2+b^2 \leq (a+b)^2 a2+b2≤(a+b)2 ,所以一定希望段数尽可能多;而且经过一些分析就可以知道,这道题希望最后一段尽可能小,而不是前面尽可能小。考虑到所有的值都是非负的,也就意味着在转移的时候希望尽可能从靠后的点转移而来。据此就可以设计一个很简单的二维dp,设 f i f_i fi 表示考虑到第 i i i 个数时的最小权值,同时用一个 l s t i lst_i lsti 记录取到这个权值时这一段的权值之和,那么只需要顺序枚举 j j j 来更新,保证基本的性质就能取到最优情况了,代码非常好写,可以得到64分。
这个形式非常像斜率优化或者单调队列,由于有特殊限制斜率优化不是很靠谱,考虑一下能否上单调队列。刚才已经提到 j j j 要尽量大,因此如果我有很多递增的决策点,只要保证当前的这个决策点取到的权值和不小于上一段的权值和(一个是递增的,一个是递减的,所以也具有单调性),那么我就可以去掉那些靠前的决策点,因为这些不可能被用到了。
因此这题的单调队列很特殊,并不是由于队头元素非法而弹掉,而是由于队头元素不够好而弹掉,事实上我们需要一直弹直到队头的下一个元素非法(即这个队列里面只有队头合法),这时候队头才是最优决策点,后面的会在之后再有机会成为决策点。
这个做这道题的时候我第一次见,所以并不会写,如果会写,这里就能得到88分。剩下的就是高精了,还需要很阴间的卡一波空间,考虑到是19年的题,如果不想写高精是非常划算的。
88分代码如下:
#include
#include
#include
using namespace std;
const int N = 5e5 + 1;
long long f[N],a[N],sum[N],lst[N],Q[N],rear,front;
int main(){
int i,j,n,t;
scanf("%d %d",&n,&t);
for(i = 1;i <= n;i++){
scanf("%lld",&a[i]);
sum[i] = sum[i - 1] + a[i];
}
memset(f,0x3f,sizeof(f));
f[0] = 0;
Q[0] = 0;
for(i = 1;i <= n;i++){
while(front < rear && lst[Q[front + 1]] + sum[Q[front + 1]] <= sum[i]) ++front;
lst[i] = sum[i] - sum[Q[front]];
f[i] = f[Q[front]] + lst[i] * lst[i];
while(front < rear && lst[Q[rear]] + sum[Q[rear]] >= lst[i] + sum[i]) --rear;
Q[++rear] = i;
}
printf("%lld\n",f[n]);
return 0;
}
考虑到20年没有大纲,这道题分类的时候其实应该被看作一个博弈论,黑题实至名归。
首先蛇的实力是排好序的,那么就可以直接讨论了。
假如当前最强的一条蛇吃完了最弱的还是最强的,那毫无疑问应该接着吃。
假如当前最强的一条蛇吃完了最弱的既不是最强的也不是最弱的,那么也可以继续吃,因为吃完之后最强的蛇没有它吃之前强,最弱的蛇也没有它吃之前弱,那么如果下一条蛇选择吃,也必然比它更容易死亡,这就意味着下一条蛇更不可能吃,那么当前最强的就可以吃了。因此只要当前最强的蛇吃完了不是最弱的都会选择吃。
归纳完前两种情况继续,假如当前最强的一条蛇吃完了是最弱的,那么就要看下一条蛇在吃了它之后会不会成为最弱的,如果不会,那么显然不能吃;如果会,下一条蛇就会变成最弱的那么就要看再下一条蛇,这就需要一直套娃,直到就剩两条蛇或者上面那种情况为止。
有了这些讨论,用一个Set维护这些蛇的下标和权值,对上面三种情况进行讨论就可以了,可以得70分,正解和蚯蚓比较像,也需要借助单调性,这个感觉会麻烦一些,加上这时候剩的时间也不多,所以这个70分还是合理的。
代码如下:
#include
#include
#include
#include
#include
using namespace std;
typedef pair<int,int> pr;
const int N = 2e6 + 1;
int a[N];
int main(){
int i,j,t,n,cnt,res,x,y,pos;
scanf("%d",&t);
for(i = 1;i <= t;i++){
scanf("%d",&n);
if(i == 1){
for(j = 1;j <= n;j++){
scanf("%d",&a[j]);
}
}
else{
for(j = 1;j <= n;j++){
scanf("%d %d",&x,&y);
a[x] = y;
}
}
set<pr> S;
for(j = 1;j <= n;j++) S.insert(make_pair(a[j],j));
cnt = 0;
while(1){
if(S.size() == 2){//直接吃了
S.erase(S.begin());
if(cnt){
if((cnt - S.size()) & 1) res = cnt + 1;
else res = cnt;
//根据奇偶讨论第一条蛇是否应该吃
}
else res = 1;
break;
}
set<pr>::iterator it = S.end();
--it;
x = it->first,pos = it->second;
y = S.begin()->first;
S.erase(it),S.erase(S.begin());
S.insert(make_pair(x - y,pos));
if(S.begin()->second != pos){
//情况1/2
if(cnt){
if((cnt - S.size()) & 1) res = cnt + 1;
else res = cnt;
//同上
break;
}
}
else if(!cnt) cnt = S.size();
//用cnt记录一下情况3下应该剩下多少条蛇
}
printf("%d\n",res);
}
return 0;
}
一个看起来非常玄的题。
首先考虑一下什么时候是无解的,如果存在某一个起点,经过一轮移动恰好回到原位置,那么无解。
如果从一维开始思考,可以很快发现,对于一次移动,可以看做由全部起点组成的一个立方体的偏移,那么仍然在这个立方体内部的点就是这一次不会离场的点集。可以对于每一维动态维护一个最左和最右的波动区间,在这个区间中的点只要会出场地就不能再统计贡献了。同时各维度之间判断是否会离场是互不影响的,所以这种算法能够比较快的进行整体统计,可以得45分。
代码如下:
#include
#include
#include
using namespace std;
inline int read_int(){
int x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x * f;
}
inline long long read_long(){
long long x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x * f;
}
const int mod = 1e9 + 7;
const int N = 1e6 + 1;
int c[N],d[N];
long long L[N],R[N],v[N],w[N];
int main(){
int i,j,n,k;
long long res = 1,sum;
bool lipu;
n = read_int(),k = read_int();
for(i = 1;i <= k;i++){
w[i] = read_long();
res = (res * w[i]) % mod;
}
for(i = 1;i <= n;i++){
c[i] = read_int(),d[i] = read_int();
}
while(1){
for(i = 1;i <= n;i++){
v[c[i]] += d[i];
L[c[i]] = min(L[c[i]],v[c[i]]);
R[c[i]] = max(R[c[i]],v[c[i]]);
sum = 1;
for(j = 1;j <= k;j++){
if(R[j] - L[j] >= w[j]){
//必定离场
printf("%lld\n",res);
return 0;
}
sum = sum * (w[j] - (R[j] - L[j]) % mod + mod) % mod;
//不同维度之间累乘,可以当求立方体体积理解
}
res = (res + sum) % mod;
}
lipu = 1;
for(i = 1;i <= k;i++){
if(v[i] != 0) lipu = 0;
}
if(lipu){
puts("-1");
return 0;
}
}
return 0;
}
最后实在不知道该说什么了,毕竟这一次总结非常完备,没有什么再可以提取核心思想的地方了。总之衷心祝各位朋友们也祝我自己2021联赛rp++!