2023暑假模拟赛
2023.7.10
\(0 + 0 + 50 + 0\)
T1
题面
在一个迷宫中有一个蛋糕。作为一个吃货,Luna 非常想吃到这块蛋糕。现在 Luna 手里有这个迷宫的地图,该地图是一个 \(r\) 行 \(c\) 列的网格图,每个格子包含了下述 4 种字符中的一种:
#表示这里是墙砖,不能通过;
.表示这里是空地,可以通过;
S表示Luna 的初始位置;
C表示蛋糕的位置。
Luna 只能在空地上行走,并且只能从一个格子走到与这个格子有公共边的相邻格子上。另外,整个地图的四周都是被墙砖所环绕的。
为了能更快吃到蛋糕,Luna 不知从哪里获得了一把次元枪,这把枪可以用来制造传送门。该枪的使用说明如下:
-
在任何时候,Luna 可以选择上下左右四个方向中的一个开一枪。当她向一个方向开枪之后,子弹会撞到这个方向上第一个遇到的墙砖,并在这个墙砖面向她的面上开启一个传送门。
-
由于能量限制,在同一时刻最多存在两个传送门。如果已经存在两个传送门,那么当Luna 再开枪时,其中一个传送门会被回收用于补充能量,回收哪一个由Luna 自己决定。当Luna 向某个传送门开枪时,会有一个新的传送门取代它,即在一块墙砖的一个面上同时只能存在一个传送门。
-
当存在两个传送门时,Luna 可以进入其中任意一个传送门,然后会从另外一个传送门走出。
由于 Luna 枪法一流,所以开枪是不耗时的。Luna 从一个格子走到任意一个相邻格子的耗时为 1,穿越传送门的耗时也为 1。
现在 Luna 把地图给了你,她想知道她吃到蛋糕的最少耗时是多少,你不忍心拒绝这样一位少女的请求,所以你绞尽脑汁也要把这个问题解决。
对于 10% 的数据,\(1 \leq r,c \leq 10\)。
对于 30% 的数据,\(1 \leq r,c \leq 50\)。
对于另外 20% 的数据,每个空地至少一个墙砖与它相邻。
对于 70% 的数据,\(1 \leq r,c \leq 200\)。
对于 100% 的数据,\(1 \leq r,c \leq 1000\),Luna 一定能吃到蛋糕。
sol
最短路。
预处理出每个点走到上,下,左,右的最远点。
走到 \((x,y)\) 时,正常走可以走到 \((x+1,y),(x,y+1),(x-1,y),(x,y-1)\),如果用传送门,只需要走到最近的一面墙就能走到上下左右任意一面墙。跑 SPFA 即可。
#pragma GCC optimize("O2")
#include<queue>
#include<cstdio>
#include<utility>
#include<algorithm>
using namespace std;
const int M=1010,inf=1e9+7;
int n,m; int id(int x,int y){return (x-1)*m+y;}
int sx,sy,ex,ey;
char mp[M][M];
const int dx[]={-1,0,1,0},dy[]={0,-1,0,1};
typedef pair<int,int> PII;
PII to[M][M][4];
#define qwq make_pair
#define nino first
#define miku second
void pre(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) if(mp[i][j]!='#'){
to[i][j][0]=(mp[i-1][j]=='#')?qwq(i,j):to[i-1][j][0];
to[i][j][1]=(mp[i][j-1]=='#')?qwq(i,j):to[i][j-1][1];
}
for(int i=n;i>=1;i--)
for(int j=m;j>=1;j--) if(mp[i][j]!='#'){
to[i][j][2]=(mp[i+1][j]=='#')?qwq(i,j):to[i+1][j][2];
to[i][j][3]=(mp[i][j+1]=='#')?qwq(i,j):to[i][j+1][3];
}
}
int dis[M][M]; queue<PII>q;
bool inq[M][M];
int dist(PII u,PII v){
return abs(u.nino-v.nino)+abs(u.miku-v.miku);
}
#define D(x) dis[x.first][x.second]
#define Q(x) inq[x.first][x.second]
#define MP(x) mp[x.first][x.second]
void SPFA(){
for(int i=0;i<M;i++)
for(int j=0;j<M;j++) dis[i][j]=inf;
dis[sx][sy]=0; q.push(qwq(sx,sy)),inq[sx][sy]=true;
while(!q.empty()){
PII u=q.front(); q.pop(),Q(u)=false;
//printf("u=(%d,%d)\n",u.nino,u.miku);
int mndis=inf;
for(int dir=0;dir<4;dir++){
PII v=to[u.nino][u.miku][dir];
if(mndis>dist(u,v)) mndis=dist(u,v);
v=qwq(u.nino+dx[dir],u.miku+dy[dir]);
if(MP(v)=='#') continue;
//printf("(%d,%d)->(%d,%d) dis=1\n",u.nino,u.miku,v.nino,v.miku);
if(D(v)>D(u)+1){
D(v)=D(u)+1;
if(!Q(v)) q.push(v),Q(v)=true;
}
}
for(int dir=0;dir<4;dir++){
PII v=to[u.nino][u.miku][dir];
//printf("(%d,%d)->(%d,%d) dis=%d\n",u.nino,u.miku,v.nino,v.miku,mndis+1);
if(D(v)>D(u)+mndis+1){
D(v)=D(u)+mndis+1;
if(!Q(v)) Q(v)=true,q.push(v);
}
}
}
}
int main(){
freopen("portals.in","r",stdin);
freopen("portals.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=0;i<=m+1;i++) mp[0][i]=mp[n+1][i]='#';
for(int i=1;i<=n;i++) scanf(" %s",mp[i]+1),mp[i][0]=mp[i][m+1]='#';
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(mp[i][j]=='S') sx=i,sy=j;
else if(mp[i][j]=='C') ex=i,ey=j;
pre(),SPFA();
printf("%d",dis[ex][ey]);
return 0;
}
T2
题面
在2036 年的欧洲,老龄化现象已经渗透到了这个社会的各个角落,随之而来的是严重的健康问题。在欧洲有关部门的建议下,老年人被派遣去递送(也是给老年人的)信件。这项建议将在全欧洲得以实施。
有关部门将欧洲划分为若干个邮区。邮区是一个由双向街道和交叉路口组成的网络,每个区内都可以雇佣任意多的老年人。每天早晨,邮递员接到包裹并按一定路线投送。路线需要满足以下要求:
-
路线的起点和终点相同;
-
同一个路口不能经过两次(体谅老年人);
-
每条街道必须被恰好一条路线覆盖(体谅老年人)。
希望你帮助有关部门求出可行的路线分配方案。
对于 35% 的数据,\(1 \leq n \leq 2000\)。
对于 65% 的数据,\(1 \leq n \leq 100000, 3 \leq m \leq 100000\)。
对于 100% 的数据,\(1 \leq n \leq 500000, 3 \leq m \leq 500000\)。
sol
先找到一条不走任何重复边的最长路径。根据题意从 \(1\) 开始走最后一定回到 \(1\),而且一定每条边都恰好走一次。记录走到的节点顺序 \(ord\)。
若有 \(ord_i = ord_j = u\) 说明 \([i,j)\) 形成一个环,输出并删除即可。
由题意,每个节点度数都是偶数,所以最后刚好删完。
用栈代替了 dfs 以防递归爆栈。
#include<cstdio>
#include<cstring>
const int M=5e5+10;
bool mark[M<<1];
int head[M],cur[M],cnte=1;
struct Edge{int to,next;}e[M<<1];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int n,m;
int stk[M],top;
int ord[M<<1],top0;
bool vis[M];
int main(){
freopen("postmen.in","r",stdin);
freopen("postmen.out","w",stdout);
memset(head,-1,sizeof head);
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;i++)
scanf("%d%d",&u,&v),add(u,v),add(v,u);
for(int i=1;i<=n;i++) cur[i]=head[i];
stk[++top]=1;
while(top>0){
int u=stk[top],t=cur[stk[top]];
if(t==-1){
ord[++top0]=u; top--;
continue;
}
cur[u]=e[cur[u]].next;
if(mark[t]) continue;
mark[t]=mark[t^1]=true;
stk[++top]=e[t].to;
}
for(int i=1;i<=top0;i++){
int cur=ord[i];
if(vis[cur]){
int tmp;
do{
tmp=stk[top--];
printf("%d ",tmp),vis[tmp]=false;
}while(tmp!=cur);
putchar('\n');
}
stk[++top]=cur,vis[cur]=true;
}
return 0;
}
T3
题面
有 \(m\) 个长度为 \(n\) 的01字符串,编号为 \(1\) 到 \(m\),每个字符串中的位都按升序或降序排列。
用 \(H(a,b)\) 表示汉明距离,即两个相同长度的字符串对应位不同的位置的个数。计算所有由三个编号不同的串 \(a,b,c\) 组成的三元组中,\(H(a,b)+H(b,c)+H(c,a)\) 最大的三元组有多少个。
输入时将每个串用两个整数 \(s_i\in[0,1]\) 和 \(f_i\in[0,n]\) 来表示,意为串的前 \(f_i\) 个位为 \(s_i\),后 \(n-f_i\) 个位为 \(1-s_i\)。
对于 30% 的数据,\(n \leq 1000, m \leq 100\)。
对于 100% 的数据,\(1 \leq n \leq 10^9, 3 \leq m \leq 10^5\)。
sol
先考虑两个数的情况。
将 \(f\) 从小到大排序就可以脱掉绝对值。
将所有数排序去重,然后设 \((f_a, s_a)<(f_b, s_b)<(f_c, s_c)\)。
- \((s_a, s_b, s_c)=(0, 0, 0) / (1, 1, 1)\)
\(H(a, b)+H(b, c)+H(c, a) = 2(f_c - f_a)\)
- \((s_a, s_b, s_c)=(0, 0, 1) / (1, 1, 0)\)
\(H(a, b)+H(b, c)+H(c, a) = 2(n + f_b - f_c)\)
- \((s_a, s_b, s_c)=(0, 1, 1) / (1, 0, 0)\)
\(H(a, b)+H(b, c)+H(c, a) = 2(n + f_a - f_b)\)
- \((s_a, s_b, s_c)=(1, 0, 1) / (0, 1, 0)\)
\(H(a, b)+H(b, c)+H(c, a) = 2n\)
到此并没有分类完,事实上还有 \((f_a, s_a)=(f_b, s_b)<(f_c, s_c)\) 和 \((f_a, s_a)<(f_b, s_b)=(f_c, s_c)\) 的情况。其实只需要去重完特判这几种情况即可,因为若有三个或以上不同的数,那么这两种情况一定不优。
所以考虑枚举 \(b\),维护 \(s = 0/1\) 时 前缀/后缀 的 个数/最大值/最小值/最大值个数/最小值个数即可。
#include<cstdio>
#include<algorithm>
const int M=1e5+10,inf=1e9+7;
struct node{
int f,s,cnt;
bool operator<(const node&o)const{return f==o.f?s<o.s:f<o.f;}
}v[M];
int n,m,mx;
int pcnt[2][M],pmax[2][M],pmin[2][M],pcmx[2][M],pcmn[2][M];
int scnt[2][M],smax[2][M],smin[2][M],scmx[2][M],scmn[2][M];
int main(){
freopen("hamming.in","r",stdin);
freopen("hamming.out","w",stdout);
for(int i=0;i<2;i++)
for(int j=0;j<M;j++) pmin[i][j]=smax[i][j]=inf;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&v[i].s,&v[i].f),v[i].cnt=1;
std::sort(v+1,v+1+m); int tot=0;
for(int i=1;i<=m;i++)
if(v[i].f!=v[i-1].f||v[i].s!=v[i-1].s||i==1) v[++tot]=v[i];
else v[tot].cnt+=v[i].cnt;
m=tot; //printf("m=%d\n",m);
for(int i=1;i<=m;i++){
int col=v[i].s;
pcnt[col][i]=pcnt[col][i-1]+v[i].cnt,pcnt[col^1][i]=pcnt[col^1][i-1];
pmax[col][i]=v[i].f,pmax[col^1][i]=pmax[col^1][i-1];
pcmx[col][i]=v[i].cnt,pcmx[col^1][i]=pcmx[col^1][i-1];
//pmin[col][i]=std::min(pmin[col][i-1],v[i].f),pmin[col^1][i]=pmin[col^1][i-1];
if(pmin[col][i-1]==inf)
pmin[col][i]=v[i].f,pcmn[col][i]=v[i].cnt;
else
pmin[col][i]=pmin[col][i-1],pcmn[col][i]=pcmn[col][i-1];
pmin[col^1][i]=pmin[col^1][i-1],pcmn[col^1][i]=pcmn[col^1][i-1];
}
for(int i=m;i>=1;i--){
int col=v[i].s;
scnt[col][i]=scnt[col][i+1]+v[i].cnt,scnt[col^1][i]=scnt[col^1][i+1];
smin[col][i]=v[i].f,smin[col^1][i]=smin[col^1][i+1];
scmn[col][i]=v[i].cnt,scmn[col^1][i]=scmn[col^1][i+1];
//smax[col][i]=std::max(smax[col][i+1],v[i].f),smax[col^1][i]=smax[col^1][i+1];
if(smax[col][i+1]==inf)
smax[col][i]=v[i].f,scmx[col][i]=v[i].cnt;
else
smax[col][i]=smax[col][i+1],scmx[col][i]=scmx[col][i+1];
smax[col^1][i]=smax[col^1][i+1],scmx[col^1][i]=scmx[col^1][i+1];
}
long long cnt=0; int ans=0;
for(int b=1;b<=m;b++){
int col=v[b].s;
//(fa,sa)<(fb,sb)<(fc,sc)
//(0,0,0),(1,1,1)
if(pcnt[col][b-1]&&scnt[col][b+1]){
//printf("case 1!\n");
int tmp=2*(smax[col][b+1]-pmin[col][b-1]);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*pcmn[col][b-1]*v[b].cnt*scmx[col][b+1];
}
//(1,0,0),(0,1,1)
if(pcnt[col^1][b-1]&&scnt[col][b+1]){
//printf("case 2!\n");
int tmp=2*(n-v[b].f+pmax[col^1][b-1]);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*pcmx[col^1][b-1]*v[b].cnt*scnt[col][b+1];
}
//(0,0,1)(1,1,0)
if(pcnt[col][b-1]&&scnt[col^1][b+1]){
//printf("case 3!\n");
int tmp=2*(n+v[b].f-smin[col^1][b+1]);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*pcnt[col][b-1]*v[b].cnt*scmn[col^1][b+1];
}
//(0,1,0),(1,0,1)
if(pcnt[col^1][b-1]&&scnt[col^1][b+1]){
//printf("case 4!\n");
int tmp=2*n;
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*pcnt[col^1][b-1]*v[b].cnt*scnt[col^1][b+1];
}
//(fa,sa)<(fb,sb)=(fc,sc)
//(0,0),(1,1)
if(pcnt[col][b-1]){
//printf("case 5!\n");
int tmp=2*(v[b].f-pmin[col][b-1]);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*pcmn[col][b-1]*v[b].cnt*(v[b].cnt-1)/2;
}
//(1,0),(0,1)
if(pcnt[col^1][b-1]){
//printf("case 6!\n");
int tmp=2*(n-v[b].f+pmax[col^1][b-1]);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*pcmx[col^1][b-1]*v[b].cnt*(v[b].cnt-1)/2;
}
//(fa,sa)=(fb,sb)<(fc,sc)
//(0,0),(1,1)
if(scnt[col][b+1]){
//printf("case 7!\n");
int tmp=2*(smax[col][b+1]-v[b].f);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*scmx[col][b+1]*v[b].cnt*(v[b].cnt-1)/2;
}
//(1,0),(0,1)
if(scnt[col^1][b+1]){
//printf("case 8!\n");
int tmp=2*(n+v[b].f-smin[col^1][b+1]);
if(tmp>ans) ans=tmp,cnt=0;
if(tmp==ans) cnt+=1ll*scmn[col^1][b+1]*v[b].cnt*(v[b].cnt-1)/2;
}
//(fa,sa)=(fb,sb)=(fc,sc)
if("smr ak ioi"){
//printf("case 9!\n");
int tmp=0;
if(tmp==ans) cnt+=1ll*v[b].cnt*(v[b].cnt-1)*(v[b].cnt-2)/6;
}
}
printf("%lld",cnt);
return 0;
}
T4
题面
程序员不能总是整天坐着编程。有时站起来离开办公桌,休息一下,与同事闲聊,甚至玩一会,也是十分好的主意。F 公司的程序员就特别喜欢一种球类游戏。
让我们想象一个在笛卡尔坐标系平面上玩的游戏。玩家坐落在点 \((0,0)\) 上,选择任意一个方向,扔出球。飞了一会儿的球在距离原点 \(d\) 的地方撞击了平面,然后按照原来的方向继续飞。在第一次撞击之后,它继续飞且在距离原点 \(2d\) 的地方第二次撞击平面,接着诸如此类(按照选定的方向继续飞行,并且每隔 \(d\) 单位距离撞击一次平面)。因为在 F 公司的所有程序员都非常强壮,所以球可以飞到无穷远处。
平面上画有 \(n\) 个圆。如果球撞击平面且撞在一个画在平面上的圆内(包括边界),那么玩家得一分。球可以一次击中多个圆,并且对于它们每一个得一分(如果球在移动过程中一共撞击了某一个圆 \(x\) 次,那么玩家从中能得到 \(x\) 分)。
计算玩家向任意方向扔一个球所能得到的最大分数。注意可能有实数坐标。
对于 30% 的数据,\(n \leq 100\)。
对于 100% 的数据,\(1 \leq n \leq 2 \times 10^4, 5 \leq d \leq 10, -10000 \leq x_i, y_i \leq 10000, 1 \leq r \leq 50\)。
sol
很厉害的一道计算几何+差分。
对于一个圆看它能和多少个 \((0,0)\) 为圆心,半径为 \(k \times d\) 的圆相交/相切。
相交/相切会产生交点。可以发现,在一条射线旋转的过程中(对应扔球方向),只有经过这些交点,才会影响到答案。
所以考虑对角度差分。若交点为 \(A,B\) 两点,那么在 \(\angle AOB\) 之间答案加一。最后寻找最大答案即可。
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long double LD;
const double pii=acos(-1);
const int M=2e4+10,N=5e5+10;
int cnt; LD pos[N],neg[N];
int n,d,ans,tot;
#define sq(x) ((x)*(x))
#define ichika(x,y) make_pair(x,y)
#define nino first
#define miku second
int main(){
scanf("%d%d",&n,&d);
for(int i=1,x,y,r;i<=n;i++){
scanf("%d%d%d",&x,&y,&r);
LD alpha=atan2(y,x);
if(alpha<0) alpha+=pii*2;
LD dis=sqrt(sq(x)+sq(y));
for(int k=ceil((dis-r)/d);k<=floor((dis+r)/d);k++){
LD R=k*d;
LD beta=acos((sq(dis)+sq(R)-sq(r))/(2*dis*R));
LD v1=alpha-beta,v2=alpha+beta;
if(v2>2*pii) v2-=2*pii,tot++;
pos[++cnt]=v1,neg[cnt]=v2;
}
}
ans=tot;
sort(pos+1,pos+1+cnt),sort(neg+1,neg+1+cnt);
for(int i=1,j=1;i<=cnt;){
LD ang=pos[i];
for(;i<=cnt&&pos[i]==ang;i++) tot++;
for(;j<=cnt&&neg[j]<ang;j++) tot--;
ans=max(ans,tot);
}
printf("%d",ans);
return 0;
}
2023.7.11
\(0 + 100 + 5 + 100\)
T1
题面
科罗拉多州的山脉是二维平面上的一条折线。这条折线由 \(N\) 个端点,\(N−1\) 段线段组成,第 \(i\) 个端点的横坐标就是 \(i\),纵坐标是 \(H_i\),纵坐标代表高度,也可以称为海拔。
罗恩打算为奶牛建造一个滑雪场,为此要在山脉上规划一条缆车线路。缆线也是一条折线,由若干段缆绳组成,起点在山脉的第一个端点,终点在最后一个端点。每段缆绳可以贴着山脉的轮廓,也可以悬浮于空中,跳过山脉上几个海拔低的端点。每段缆绳的水平跨度有限制,不能超过给定的整数 \(K\)。罗恩需要在每段缆绳的端点处修建支柱,用来固定缆绳。
请帮助他规划一下,选择在山脉的哪些端点上修建,才能使得支柱数量最少?注意,根据题意,起点和终点上是一定要修建的。
\(2 \le N \le 5000\),\(1 \le K \le N − 1\),\(0\le H_i \le 10^9\)。
sol
设 \(\operatorname{slope}(i,j)\) 为 \((i,h_i),(j,h_j)\) 两点间连线的斜率。如果 \(i\) 能连线到 \(j\) 说明 \(\operatorname{slope}(i,j) \ge \max\limits_{p=i+1}^{j} \operatorname{slope}(i,p)\)。而 \(n \leq 5000\) 故直接枚举一个柱子能转移到那些柱子即可。
内存只有 64MB 所以开不了二维数组。
#include<cstdio>
#include<cstring>
const int M=5015;
const double inf=1e18;
template<typename T>
inline T max(T A,T B){
return A>B?A:B;
}
template<typename T>
inline T min(T A,T B){
return A<B?A:B;
}
//double mx[M][M];
int n,k,h[M];
double slope(int i,int j){
return (double)(h[i]-h[j])/(double)(i-j);
}
int f[M];
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
for(int i=1;i<=n;i++) f[i]=1e9+7; f[1]=1;
for(int i=1;i<n;i++){
double mx=-inf;
for(int j=i+1;j<=min(n,i+k);j++){
double s=slope(i,j);
if(s>=mx) f[j]=min(f[j],f[i]+1),mx=s;
}
}
printf("%d",(f[n]==1e9+7)?-1:f[n]);
return 0;
}
T2
题面
[USACO06MAR] Milk Team Select G
Farmer John 的 \(N(1 \le N \le 500)\) 头奶牛打算参加一场世界级的产奶比赛 (Multistate Milking Match-up,MMM),他们已经摸清了其他队的实力。他们的总产奶量只要大于等于 \(X\) 加仑(\(1 \leq X \leq 10^6\)),就能赢得胜利。
每头奶牛都能为全队贡献一定量的牛奶,数值在 \(-10^4\) 到 \(10^4\) 加仑之间(为啥有负数?因为有些奶牛会打翻其他奶牛产的牛奶)。
MMM 的目标是通过合作,增进家庭成员间的默契。为了支持比赛精神,奶牛们希望在赢得比赛的前提下,有尽可能多对奶牛间存在直系血缘关系。当然,所有奶牛都是女性,因此这里的直系血缘关系就是母女关系。
现在 FJ 摸清了所有奶牛间的血缘关系,希望算出一个团队在赢得胜利的前提下,最多有多少对奶牛存在血缘关系。注意:如果一个团队由某头奶牛和她的母亲和外祖母组成的话,这个团队只有两对血缘关系(她和她的母亲,她的母亲和外祖母)。
sol
加上 \(0\) 号点,这些牛构成一棵树,考虑树形 dp。
设 \(f_{u,i,0/1}\) 为在 \(u\) 子树内,有 \(i\) 个血缘关系,且 不选/选 第 \(u\) 头牛的最大产奶量。
\(f_{u,i+j,0} \gets f_{u,i,0}+\max(f_{v,j,0},f_{v,j,1})\)
\(f_{u,i+j,1} \gets f_{u,i,1}+f_{v,j,0}\)
\(f_{u,i+j+1,1} \gets f_{u,i,1}+f_{v,j,1}\)
然后取最大的 \(i\) 满足 \(f_{0,i,0} \geq X\) 即可。时间复杂度 \(O(n^3)\)。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M=1010,inf=1e9+7;
int n,X,a[M];
int f[M][M][2];
int head[M],cnte;
struct Edge{int to,next;}e[M];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int siz[M];
void DP(int u){
f[u][0][0]=0,f[u][0][1]=a[u];
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to; DP(v);
for(int p=siz[u];p>=0;p--)
for(int q=siz[v];q>=0;q--){
f[u][p+q][0]=max(f[u][p+q][0],f[u][p][0]+max(f[v][q][0],f[v][q][1]));
f[u][p+q+1][1]=max(f[u][p+q+1][1],f[u][p][1]+f[v][q][1]);
f[u][p+q][1]=max(f[u][p+q][1],f[u][p][1]+f[v][q][0]);
}
siz[u]+=siz[v]+1;
}
}
int main(){
memset(head,-1,sizeof head);
memset(f,0xc0,sizeof f);
scanf("%d%d",&n,&X);
for(int i=1,f;i<=n;i++){
scanf("%d%d",&a[i],&f);
add(f,i);
}
DP(0); int ans=-1;
for(int i=0;i<=n;i++)
if(f[0][i][0]>=X) ans=i;
printf("%d",ans);
return 0;
}
T3
题面
小 Z 离开家的时候忘记带走了钱包,掉下的硬币在桌子上排成了一列。正在等着哥哥回来的小 D 坐在桌子旁边,无聊地翻着桌子上的硬币。
出于某种爱好,小 D 一次一定会同时翻转 M 枚硬币。由于小 D 是一个爱动脑的小学生,这样进行了若干次之后她很快想到了一个问题:有多少种方法能够在 K 次翻转后把硬币由原来的状态变成现在这样呢?
因为小 D 是个好学的小学生,她只需要你告诉她方案数对 1000000007 取模的值以方便她进行验算就可以了。
对于 30% 的数据,\(N \leq 4, 0 \leq K \leq 5\);
对于 60% 的数据,\(N \leq 10\);
对于 100% 的数据,\(1 \leq N \leq 100, 0 \leq K \leq 100, 0 \leq M \leq N\)。
sol
dp。设 \(f_{i,j}\) 为翻了 \(i\) 次,剩下 \(j\) 个硬币不同的方案数。
枚举第 \(i\) 次翻硬币后使得 \(p\) 个从不同变得相同,那么翻后 \(j' = j - 2p + m\)。
选出 \(j\) 个原来不同的方案数为 \(C_j^p\),\(m-j\) 个原来相同的方案数为 \(C_{n-j}^{m-p}\)。
\(f_{i,j'} \gets f_{i,j'} + f_{i-1,j}C_j^pC_{n-j}^{m-p}\)
#include<cstdio>
#include<cstring>
#define int long long
const int M=221,mod=1e9+7;
int n,k,m,f[M][M];
char A[M],B[M]; int cnt;
int C[M][M];
void pre(){
for(int i=0;i<M;i++) C[i][i]=C[i][0]=1;
for(int i=1;i<M;i++)
for(int j=1;j<i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
signed main(){
pre();
scanf("%lld%lld%lld",&n,&k,&m);
scanf(" %s",A+1),scanf(" %s",B+1);
for(int i=1;i<=n;i++)
if(A[i]!=B[i]) cnt++;
f[0][cnt]=1;
for(int i=1;i<=k;i++)
for(int j=0;j<=n;j++)
for(int p=0;p<=m&&p<=j;p++){
int c=j-2*p+m;
if(c>=0&&c<=n)
f[i][c]=1ll*(f[i][c]+f[i-1][j]*C[j][p]%mod*C[n-j][m-p]%mod)%mod;
}
printf("%lld",f[k][0]);
return 0;
}
T4
题面
小 Z 是一个爱好数学的小学生。最近,他在研究一些关于整数数列的性质。
为了方便他的研究,小Z希望实现一个叫做“Open Continuous Lines Processor”的数列编辑器。
一开始,数列编辑器里没有数字,只有一个光标。这个数列编辑器需要支持五种操作。
I x在当前光标前插入数字 \(x\)。D删除当前光标前的数字。L光标向前移动一个数字。R光标向后移动一个数字。Q k设光标之前的数列是 \(\{a_1,a_2,\cdots,a_n\}\),输出第 \(k\) 位及之前最大的前缀和,保证 \(k\leqslant n\)。
对于 \(50\%\) 的数据,\(N\leqslant1000\);
对于 \(80\%\) 的数据,\(N\leqslant10^5\);
对于 \(100\%\) 的数据,\(N\leqslant10^6\),插入的数字绝对值大小不会超过 \(1000\)。
题目保证不会在数列编辑器为空时进行 D 操作。
sol
因为是前缀操作所以不必要 splay,用链表就行。
每次右移的时候维护节点信息即可。
#include<cstdio>
const int M=1e6+10,inf=1e9+7;
int n;
inline int max(int A,int B){return A>B?A:B; }
struct OCLP{
struct node{
int pre,nxt,mx,sum,val;
}l[M];
int head,tail,qwq,cur,id[M],ans[M];
void L(){cur=l[cur].pre;}
void R(){
cur=l[cur].nxt;
l[cur].sum=l[l[cur].pre].sum+l[cur].val;
l[cur].mx=max(l[l[cur].pre].mx,l[cur].sum);
ans[id[cur]=id[l[cur].pre]+1]=l[cur].mx;
}
void I(int x){
l[++qwq]=(node){head,tail,-inf,-inf,x};
l[qwq].pre=cur,l[qwq].nxt=l[cur].nxt;
l[l[qwq].nxt].pre=l[cur].nxt=qwq;
R();
}
void D(){
l[l[cur].pre].nxt=l[cur].nxt;
l[l[cur].nxt].pre=l[cur].pre;
L();
}
int Q(int k){return ans[k];}
OCLP(){
head=++qwq,tail=++qwq;
l[head]=(node){head,tail,-inf,0,0};
l[tail]=(node){head,tail,-inf,0,0};
cur=head;
}
}oclp;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
char opt; int x;
scanf(" %c",&opt);
if(opt=='I'){
scanf("%d",&x); oclp.I(x);
}else if(opt=='D'){
oclp.D();
}else if(opt=='L'){
oclp.L();
}else if(opt=='R'){
oclp.R();
}else if(opt=='Q'){
scanf("%d",&x);
printf("%d\n",oclp.Q(x));
}
}
return 0;
}
2023.7.12
\(100 + 0 + 100 + 90\)
T1
题面
在某个遥远的国家里,有 \(n\) 个城市。编号为 \(1, 2, \cdots, n\) 。 这个国家的政府修建了m条双向的公路。每条公路连接着两个城市。沿着某条公路,开车从一个城市到另一个城市,需要花费一定的汽油。
开车每经过一个城市,都会被收取一定的费用(包括起点和终点城市)。所有的收费站都在城市中,在城市间的公路上没有任何的收费站。
小红现在要开车从城市 \(u\) 到城市 \(v(1 \leq u,v \leq n)\)。她的车最多可以装下 \(s\) 升的汽油。在出发的时候,车的油箱是满的,并且她在路上不想加油。
在路上,每经过一个城市,她要交一定的费用。如果她某次交的费用比较多,她的心情就会变得很糟。所以她想知道,在她能到达目的地的前提下,她交的费用中最多的一次最少是多少。这个问题对于她来说太难了,于是她找到了聪明的你,你能帮帮她吗?
对于 60% 的数据,满足 \(n \leq 200, m \leq 19900, s \leq 200\),没有一条边连接着两个相同的城市。
对于 100% 的数据,满足 \(n \leq 10000, m \leq 50000, s \leq 10^9\),可能有两条边连接着相同的城市。
对于 100% 的数据,满足 \(c_i \leq 10^9, f_i \leq 10^9\)。
sol
二分最小费用 \(mid\),从 \(u\) 点开始跑最短路,费用超过 \(mid\) 的点不走。
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M=1e5+10;
const long long inf=1e18;
int n,m,U,V,s,cost[M];
struct Graph{
int head[M],cnte;
int next[M],to[M],dis[M];
void add(int u,int v,int w){
to[++cnte]=v,next[cnte]=head[u];
dis[cnte]=w,head[u]=cnte;
}
Graph(){
memset(head,-1,sizeof head);
cnte=1;
}
}G;
long long dis[M]; bool vis[M];
typedef pair<long long,int> pli;
priority_queue<pli,vector<pli>,greater<pli>>q;
#define qwq make_pair
void Dijkstra(int x){
for(int i=1;i<=n;i++) dis[i]=inf,vis[i]=false;
q.push(make_pair(dis[U]=0,U));
while(!q.empty()){
int u=q.top().second; q.pop();
if(vis[u]) continue;
vis[u]=true;
for(int i=G.head[u];~i;i=G.next[i]){
int v=G.to[i];
if(dis[v]>dis[u]+G.dis[i]&&cost[v]<=x)
q.push(qwq(dis[v]=dis[u]+G.dis[i],v));
}
}
}
bool check(int x){
Dijkstra(x);
return dis[V]<=s;
}
int mxcost;
int main(){
scanf("%d%d%d%d%d",&n,&m,&U,&V,&s);
for(int i=1;i<=n;i++)
scanf("%d",&cost[i]),mxcost=max(mxcost,cost[i]);
for(int i=1,u,v,w;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
G.add(u,v,w),G.add(v,u,w);
}
int L=max(cost[U],cost[V]),R=mxcost,ans=-1;
while(L<=R){
int mid=(L+R)>>1;
if(check(mid)) ans=mid,R=mid-1;
else L=mid+1;
}
printf("%d",ans);
return 0;
}
T2
题面
为了封印辉之环,古代塞姆利亚大陆的人民在异空间中建造了一座设备塔。
简单的说,这座设备塔是一个漂浮在异空间中的圆柱体,圆柱体两头的圆是计算核心,而侧面则是
传输信息所用的数据通道,划分成 \(n \times m\) 个区块。
然而,随着工作的继续进行,他们希望把侧面的一部分区块也改造成其他模块。然而,任何时候都
必须保证存在一条数据通道,能从圆柱体的一端通向另一端。
由于无法使用辉之环掌控下的计算系统,他们寻求你的帮助来解决这个问题。他们将逐个输入想要
改造的区域,而你则执行所有可行的改造并忽略可能导致数据中断的改造。
• 对于分值为 30 的子任务1,保证 \(N,M \leq 100, K \leq 5000\)。
• 对于分值为 30 的子任务2,保证 \(N,M \leq 3000, K \leq 5000\)。
• 对于分值为 40 的子任务3,保证 \(N,M \leq 3000, K \leq 300000\)。
sol
破环为链,把矩形复制一遍接到后面。
当加入一个点时,另一个矩形的对应点也要加入。
如果加入后存在八联通块和左右边界相接,那么这次操作不合法。
可以用并查集模拟。
#include<cstdio>
const int N=3010,M=2*3010*3010;
const int dx[]={-1,0,1,1,1,0,-1,-1},
dy[]={-1,-1,-1,0,1,1,1,0};
bool mark[M];
int n,m,k,cnt,ans;
int id(int x,int y){
return (x-1)*2*m+y;
}
int fa[M]; int dfn[M];
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y){
fa[find(x)]=find(y);
}
void work(int x,int y){
int id0=id(x,y); mark[id0]=true;
for(int dir=0;dir<8;dir++){
int xx=x+dx[dir],yy=y+dy[dir];
if(xx<1||xx>n) continue;
if(yy<1) yy+=2*m; if(yy>2*m) yy-=2*m;
int id1=id(xx,yy);
if(mark[id1]) merge(id0,id1);
}
}
int main(){
scanf("%d%d%d",&n,&m,&k),cnt=2*n*m;
for(int i=1;i<=cnt;i++) fa[i]=i;
for(int i=1,x,y;i<=k;i++){
scanf("%d%d",&x,&y);
for(int dir=0;dir<8;dir++){
int xx=x+dx[dir],yy=y+dy[dir];
if(xx<1||xx>n) continue;
if(yy<1) yy+=2*m; if(yy>2*m) yy-=2*m;
int id0=id(xx,yy);
if(mark[id0]) dfn[find(id0)]=i;
}
bool flag=true;
for(int dir=0;dir<8;dir++){
int xx=x+dx[dir],yy=y+dy[dir]+m;
if(xx<1||xx>n) continue;
if(yy<1) yy+=2*m; if(yy>2*m) yy-=2*m;
int id0=id(xx,yy);
if(dfn[find(id0)]==i&&mark[id0]){flag=false;break;}
}
if(flag)
ans++,work(x,y),work(x,y+m);
}
printf("%d",ans);
return 0;
}
T3
题面
小 K 和同学们期待已久的暑假即将到来。然而小 K 的班主任兼德育处主任却莫名失踪了一个月。据小道消息,他在高考后才会出现。但现在德育处有诸多繁杂的工作,确定每个班的“三好学生”便是最令人头痛的。
HY 中学的大 Boss 圆规要求 \(N\) 个班的班主任按照学生的综合素质推荐学生。但他深深地知道,每个班主任都会无一例外地使用 xls 把 \(M\) 名学生数次月考成绩相加排序,然后从高到底依次录取。圆规知道每个班学生成绩的排序和每个学生得到该荣誉的“负”能量 \(A_{i,j}\)。显然,“负”能量也是有正有负的。他不希望有些学生拿到“三好学生”后依然肆无忌惮地拔掉教室里监控的探头,或是在晚餐时间在篮球场上尽情玩耍(这在 HY 中学是会被通报批评的)。他希望能够多发扬正能量。
圆规从不在乎“三好学生”的数量,但是他却不得不考虑学生的感受。学生在学生会上
(1)提出了 \(K\) 条无理的要求,每条要求对于班级 \(u,v\),要求班级 \(u\) 的三好学生数量 \(S_u\) 和班级 \(v\) 三好学生数量 \(S_v\),满足 \(S_u - S_v \leq w\)。
(2)提出每个班至少一张奖状。
鉴于 HY 中学的高一高二学生在 5 月下旬某一个酷热的晚上,忍无可忍围起了整栋教学楼,并且齐声高声叫喊:“开空调!”但学校善后处理十分不得力。圆规决定全盘接受学生的建议,以平息学生的怒气。但是,他还是想知道最小的负能量。
20% 的数据 \(n,m \leq 10\)。
50% 的数据 \(n,m \leq 30, K \leq 500\)。
70% 的数据 \(n,m \leq 50, K \leq 2000\)。
100% 的数据 \(n \leq 900, m \leq 60, K \leq 20000, |A_{i,j}| \leq 1000, w \leq 1000\)。
sol
看着像差分约束,但是带了别的条件。其实是最小割。
首先对每个班建立一条链,每个学生是上面的一个点。连边 \((i,j) \to (i,j+1)\),容量是 \(\sum\limits_{k=1}^j a_{i,k}\)。特别的 \((i,m) \to T\),容量为 \(\sum\limits_{k=1}^m a_{i,k}\)。如果割掉 \((i,j) \to (i,j+1)\) 的边就说明第 \(i\) 个班选了 \(j\) 个学生。
而每个班至少一张奖状,所以连边 \(S \to (i,1)\),容量为 \(inf\)。
对于 \(S_u - S_v \leq w\) 这个限制,连边 \((u,x) \to (v,x-w)\),容量为 \(inf\)。这样如果割掉了 \((u,x) \to (u,x+1)\),那么 \((u,x)\) 仍然通过 \((v,x-w)\) 与汇点联通,这样第 \(v\) 个班割掉的边就在 \(x-w\) 之后。而每个班只割一条,所以就保证前 \(x-w\) 个学生都被选中了。
有可能出现负容量。设 \(M = \min\limits_{i=1}^n \sum\limits_{k=1}^j A_{i,k}\),然后令所有流量都减去 \(M\),最后计算答案的时候加上 \(n \times M\) 即可。
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M=2e5+10,inf=1e9+7;
int head[M],cnte=1;
struct Edge{int to,next,cap,flow;}e[M];
void add(int u,int v,int w){
e[++cnte]=(Edge){v,head[u],w,0};
head[u]=cnte;
}
void addedge(int u,int v,int w){
add(u,v,w),add(v,u,0);
}
int s,t,dis[M];
queue<int>q;
bool bfs(){
for(int i=s;i<=t;i++) dis[i]=0;
q.push(s),dis[s]=1;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(!dis[v]&&e[i].cap>e[i].flow)
dis[v]=dis[u]+1,q.push(v);
}
}
return dis[t];
}
int dfs(int u,int in){
if(u==t) return in;
int out=0;
for(int i=head[u];~i&∈i=e[i].next){
int v=e[i].to;
if(e[i].cap>e[i].flow&&dis[v]==dis[u]+1){
int res=dfs(v,min(in,e[i].cap-e[i].flow));
e[i].flow+=res,e[i^1].flow-=res;
in-=res,out+=res;
}
}
if(!out) dis[u]=-114514;
return out;
}
int Dinic(){
int ans=0;
while(bfs()) ans+=dfs(s,inf);
return ans;
}
int n,m,k; int id(int x,int y){return (x-1)*m+y;}
int a[1010][110],minn=inf;
int main(){
freopen("honor.in","r",stdin);
freopen("honor.out","w",stdout);
memset(head,-1,sizeof head);
scanf("%d%d%d",&n,&m,&k);
s=0,t=n*m+1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]),a[i][j]+=a[i][j-1];
minn=min(minn,a[i][j]);
}
for(int i=1;i<=n;i++) addedge(s,id(i,1),inf);
for(int i=1;i<=n;i++)
for(int j=1;j<m;j++)
addedge(id(i,j),id(i,j+1),a[i][j]-minn);
for(int i=1;i<=n;i++) addedge(id(i,m),t,a[i][m]-minn);
for(int i=1,u,v,w;i<=k;i++){
scanf("%d%d%d",&u,&v,&w);
for(int j=w+1;j<=m;j++) addedge(id(u,j),id(v,j-w),inf);
}
int cut=Dinic();
printf("%d",cut+minn*n);
return 0;
}
T4
题面
外卖店一共有 \(n\) 种食物,分别从 \(1\) 到 \(n\) 编号。第 \(i\) 种食物有固定的价钱 \(p_i\) 和保质期 \(s_i\)。第 \(i\) 种食物会在 \(s_i\) 天后过期。JYY 是不会吃过期食物的。
比如 JYY 如果今天点了一份保质期为 \(1\) 天的食物,那么 JYY 必须在今天或者明天把这个食物吃掉,否则这个食物就再也不能吃了。保质期可以为 \(0\) 天,这样这份食物就必须在购买当天吃掉。
JYY 现在有 \(m\) 块钱,每一次叫外卖需要额外付给送外卖小哥外送费 \(f\) 元。
送外卖的小哥身强力壮,可以瞬间给 JYY 带来任意多份食物。JYY 想知道,在满足每天都能吃到至少一顿没过期的外卖的情况下,他可以最多宅多少天呢?
对于全部的测试点,保证 \(1 \leq n \leq 200\),\(0 \leq s_i \leq 10^{18}\),\(1 \leq f, p_i, m \leq10^{18}\)。
sol
先将一些贵但是保质期短的食物去掉。
将所有食物按保质期排序,然后顺序插入栈中。如果栈顶的价格大于当前的价格(因为排序所以栈里的保质期短)就弹出栈顶。
然后假如你要买一次食物度过 \(T\) 天,那么第 \(1 \sim s_1\) 天吃保质期为 \(s_1\) 的食物,第 \(s_1+1 \sim s_2\) 天吃保质期为 \(s_2\) 的食物,以此类推。
然后如果点了 \(x\) 次外卖,那么这 \(x\) 次越平均越好。如果不平均,那么为了撑过最长一段时间就要买更贵的食物,显然不优。
然后就是确定来的次数。事实上,宅的天数与点外卖次数成单峰函数关系。感性理解,如果小于某个次数,那么就要买更贵的食物;如果大于某个次数,那么买食物省下的钱都拿去付外卖费了。如果买 \(x\) 次花在食物上的钱减去买 \(x+1\) 次花在食物上的钱小于外卖费 \(f\),那么显然买 \(x\) 次更优。故三分答案即可。
要开__int128。
#include<cstdio>
#include<algorithm>
using namespace std;
const int M=210;
__int128 read(){
__int128 x=0; int f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void print(__int128 x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9) print(x/10);
putchar(x%10+'0');
}
struct food{
__int128 p,s;
bool operator<(const food &o)const{return s<o.s;}
}F[M],stk[M]; int top;
int n;
__int128 m,f;
__int128 calc(__int128 T){
__int128 M=m-T*f,las=0,ans=0;
for(int i=1;i<=n;i++){
__int128 day=min(M/stk[i].p,(stk[i].s-las)*T);
if(day<=0) break;
M-=day*stk[i].p;
las+=day/T,ans+=day;
}
return ans;
}
int main(){
freopen("food.in","r",stdin);
freopen("food.out","w",stdout);
m=read(),f=read(); scanf("%d",&n);
for(int i=1;i<=n;i++)
F[i].p=read(),F[i].s=read()+1;
sort(F+1,F+1+n);
for(int i=1;i<=n;i++){
while(top>0&&stk[top].p>=F[i].p) top--;
stk[++top]=F[i];
}
n=top; for(int i=1;i<=n;i++) F[i]=stk[i];
__int128 L=1,R=m/f,ans=0;
while(L<=R){
__int128 m1=L+(R-L)/3,m2=R-(R-L)/3;
__int128 p1=calc(m1),p2=calc(m2);
if(p1<p2) L=m1+1,ans=max(ans,p1);
else R=m2-1,ans=max(ans,p2);
}
print(ans);
return 0;
}
2023.7.13
\(20 + 80 + 0 + 50\)
T1
题面
Samjia 和 Peter 不同,他喜欢玩树。所以 Peter送给他一颗大小为 \(n\) 的树,节点编号从 \(1\) 到 \(n\)。
Samjia 要给树上的每一个节点赋一个 \([1,m]\) 之间的权值,并使得有边相连的两个节点的权值之差的绝对值 \(\ge k\)。请你告诉 Samjia 有多少种不同的赋值方案,只用求出答案对 \(10^9 + 7\) 取模得到的结果。
多组数据。
| 测试点编号 | \(m \leq\) | 特殊约定 |
|---|---|---|
| 1,2 | \(100\) | 无 |
| 3,4 | \(10000\) | 无 |
| 5,6 | \(10^9\) | 第 \(2 \sim n\) 号节点与 \(1\) 号节点直接相连 |
| 7,8 | \(10^9\) | 第 \(i\) 号节点与第 \(i+1\) 号节点直接相连 |
| 9,10 | \(10^9\) | 无 |
对于所有数据,\(T \leq 10, n \leq 100, k \leq 100, m \leq 10^9\)。
sol
设 \(f_{u,x}\) 表示在 \(u\) 点权值为 \(x\),\(u\) 的子树的方案树。
\(f_{u,x} = \prod\limits_{v \in son(u)} ( \sum\limits_{|x-y| \ge k} f_{v,y})\)。
首先注意到括号内可以前缀和优化。
然后注意到 \(f_{u,x} = f_{u,m-x+1}\),因为将所有点的点权 \(v_i \gets m - v_i + 1\),仍然是一颗合法的树。
然后注意到 \(\forall x \in [(n-1) \times k + 1, m - (n-1) \times k]\),\(f_{u,x}\) 的值都相同,因为给所有点的点权 \(+1\),如果没有点权 \(> m\),那么仍然是合法的。
所以只需要求出 \(x \in [1, (n-1) \times k + 1]\) 的 \(f_{u,x}\) 及其前缀和,求 \([1,m]\) 的前缀和时分类讨论即可。
#include<cstdio>
#include<cstring>
#define int long long
const int M=110,mod=1e9+7;
inline int min(int A,int B){
return A<B?A:B;
}
int head[M],cnte;
struct Edge{int to,next;}e[M<<1];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int T,n,m,k,mx;
int f[M][M*M],g[M][M*M];
int qsum(int u,int p){
if(p<=0) return 0;
if(p<=mx) return g[u][p];
if(p<=m-mx) return g[u][mx]+(f[u][mx]*(p-mx)%mod)%mod;
if(p>m-mx) return (g[u][mx]-g[u][m-p]+qsum(u,m-mx)+mod)%mod;
}
void DP(int u,int fa){
for(int j=1;j<=mx;j++) f[u][j]=1;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to; if(v==fa) continue;
DP(v,u);
for(int x=1;x<=mx+1;x++){
long long tmp=0;
if(x-k>0) tmp=(tmp+qsum(v,x-k))%mod;
if(x+k<=m) tmp=(tmp+qsum(v,m)-qsum(v,x+k-1)+mod)%mod;
f[u][x]=f[u][x]*tmp%mod;
}
}
for(int j=1;j<=mx;j++) g[u][j]=(g[u][j-1]+f[u][j])%mod;
}
signed main(){
freopen("label.in","r",stdin);
freopen("label.out","w",stdout);
scanf("%lld",&T);
while(T--){
memset(head,-1,sizeof head); cnte=1;
memset(f,0,sizeof f),memset(g,0,sizeof g);
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1,u,v;i<n;i++){
scanf("%lld%lld",&u,&v);
add(u,v),add(v,u);
}
if(k==0){
int ans=1;
for(int i=1;i<=n;i++) ans=1ll*ans*m%mod;
printf("%lld\n",ans);
continue;
}
mx=(m<=10000)?m:(n-1)*k+1;
DP(1,0);
printf("%lld\n",qsum(1,m));
}
return 0;
}
T2
题面
Ducky 有一个大小为 \(n \times m\) 的农场,Ducky 要在他家的农场上开垦一片正方形鱼池来养鱼。但是在此之前,他的农场已经种下了很多课树。他要在不把树砍断的前提下,使得鱼池面积尽可能大。
他做了 \(t\) 种设计方案,每种方案会限制鱼池不能建在左上角为 \((x_1, y_1)\),右下角为 \((x_2, y_2)\) 的矩形的外面,对于每种设计方案,你只需要回答鱼池的最大边长是多少即可,如果不能建造鱼池,回答 \(0\) 即可。
\(n \leq 1000, T \leq 10^6\)。
sol
难以存下每个矩形中最大的正方形的边长。考虑二分答案,用一个 \(\log n\) 转化为判定性问题。
dp 求出 \(f_{i,j}\) 为以 \((i,j)\) 为左上角的最大正方形边长。
\(f_{i,j} = \min(f_{i+1,j}, f_{i,j+1}, f_{i+1,j+1}) + 1\)。
对于每次询问,二分矩形内是否存在边长为 \(mid\) 的正方形。当 \(\max\limits_{x_1 \leq x \leq x_2 - mid + 1, y_1 \leq y \leq y_2 - mid + 1} f_{x,y} \geq mid\) 时说明 \(mid\) 合法。
所以需要一个快速求出矩形最大值的数据结构。树套树时间复杂度为 \(O(n \log^3 n)\),会被卡掉后 4 个点。因为是静态查询所以使用二维 ST 表,预处理 \(O(n^2 \log^2 n)\),查询 \(O(1)\),总复杂度 \(O(n^2 \log^2 n + T \log n)\)。
#pragma GCC optimize("Ofast")
#include<cmath>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int M=1010,N=1e6+10;
int lg2[M];
inline int read(){
int v=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') v=v*10+ch-'0',ch=getchar();
return v;
}
int n,m,t;
bool mp[M][M]; int mxs[M][M];
/*
int cntp;
struct node{
int lc,rc,val;
}tr[N<<4];
void pushup(int k){
tr[k].val=max(tr[tr[k].lc].val,tr[tr[k].rc].val);
}
void insert(int &k,int l,int r,int p,int v){
if(!k) k=++cntp;
if(l==r){
tr[k].val=max(tr[k].val,v);
return;
}
int mid=(l+r)>>1;
if(p<=mid) insert(tr[k].lc,l,mid,p,v);
else insert(tr[k].rc,mid+1,r,p,v);
pushup(k);
}
int query(int k,int L,int R,int l,int r){
if(!k) return 0;
if(L<=l && r<=R) return tr[k].val;
int mid=(l+r)>>1;
if(R<=mid) return query(tr[k].lc,L,R,l,mid);
else if(L>mid) return query(tr[k].rc,L,R,mid+1,r);
else return max(query(tr[k].lc,L,R,l,mid),query(tr[k].rc,L,R,mid+1,r));
}
struct node0{
int l,r,rt;
}tr0[M<<2];
void build(int k,int l,int r){
tr0[k].l=l,tr0[k].r=r;
if(l==r) return;
int mid=(l+r)>>1;
build(k<<1,l,mid),build(k<<1|1,mid+1,r);
}
void ins(int k,int x,int y,int v){
insert(tr0[k].rt,1,m,y,v);
if(tr0[k].l==tr0[k].r) return;
int mid=(tr0[k].l+tr0[k].r)>>1;
if(x<=mid) ins(k<<1,x,y,v);
else ins(k<<1|1,x,y,v);
}
int qry(int k,int l0,int r0,int l1,int r1){
if(l0<=tr0[k].l&&tr0[k].r<=r0)
return query(tr0[k].rt,l1,r1,1,m);
int mid=(tr0[k].l+tr0[k].r)>>1;
if(r0<=mid) return qry(k<<1,l0,r0,l1,r1);
else if(l0>mid) return qry(k<<1|1,l0,r0,l1,r1);
else return max(qry(k<<1,l0,r0,l1,r1),qry(k<<1|1,l0,r0,l1,r1));
}*/
int st[11][11][M][M];
void build_st(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) st[0][0][i][j]=mxs[i][j];
for(int i=1;(1<<i)<=n;i++)
for(int k=1;k<=n-(1<<i)+1;k++)
for(int l=1;l<=m;l++)
st[i][0][k][l]=max(st[i-1][0][k][l],st[i-1][0][k+(1<<(i-1))][l]);
for(int j=1;(1<<j)<=m;j++)
for(int k=1;k<=n;k++)
for(int l=1;l<=m-(1<<j)+1;l++)
st[0][j][k][l]=max(st[0][j-1][k][l],st[0][j-1][k][l+(1<<(j-1))]);
for(int i=1;(1<<i)<=n;i++)
for(int j=1;(1<<j)<=m;j++)
for(int k=1;k<=n-(1<<i)+1;k++)
for(int l=1;l<=m-(1<<j)+1;l++){
st[i][j][k][l]=max(max(st[i-1][j-1][k][l],st[i-1][j-1][k+(1<<(i-1))][l]),
max(st[i-1][j-1][k][l+(1<<(j-1))],st[i-1][j-1][k+(1<<(i-1))][l+(1<<(j-1))]));
//printf("(%d,%d,%d,%d):\n (%d,%d,%d,%d)\n (%d,%d,%d,%d)\n (%d,%d,%d,%d)\n (%d,%d,%d,%d)\n",
// i,j,k,l,i-1,j-1,k,l,i-1,j-1,k+(1<<(i-1)),l,i-1,j-1,k,l+(1<<(j-1)),i-1,j-1,k+(1<<(i-1)),l+(1<<(j-1)));
}
// for(int i=0;(1<<i)<=n;i++)
// for(int j=0;(1<<j)<=m;j++)
// for(int k=1;k<=n-(1<<i)+1;k++)
// for(int l=1;l<=m-(1<<j)+1;l++)
// printf("st[%d][%d][%d][%d] = %d\n",i,j,k,l,st[i][j][k][l]);
}
int qry(int x0,int x1,int y0,int y1){
int k1=lg2[x1-x0+1],k2=lg2[y1-y0+1];
//int k1=log2(x1-x0+1),k2=log2(y1-y0+1);
int tmp=max(max(st[k1][k2][x0][y0],st[k1][k2][x1-(1<<k1)+1][y0]),
max(st[k1][k2][x0][y1-(1<<k2)+1],st[k1][k2][x1-(1<<k1)+1][y1-(1<<k2)+1]));
//printf("Qry(%d,%d,%d,%d) = %d, k1 = %d, k2 = %d\n",x0,y0,x1,y1,tmp,k1,k2);
return tmp;
}
int main(){
lg2[0]=-1;
for(int i=1;i<M;i++) lg2[i]=lg2[i>>1]+1;
freopen("square.in","r",stdin);
freopen("square.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) mp[i][j]=read();
for(int i=n;i>=1;i--)
for(int j=m;j>=1;j--)
if(mp[i][j])
mxs[i][j]=min(min(mxs[i+1][j],mxs[i][j+1]),mxs[i+1][j+1])+1;
/*
build(1,1,n);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(mp[i][j]) ins(1,i,j,mxs[i][j]);*/
build_st();
t=read();
for(int i=1,x0,y0,x1,y1;i<=t;i++){
x0=read(),y0=read(),x1=read(),y1=read();
/*
int L=1,R=min(min(x1-x0+1,y1-y0+1),qry(1,x0,x1,y0,y1)),ans=0;
while(L<=R){
int mid=(L+R)>>1;
if(qry(1,x0,x1-mid+1,y0,y1-mid+1)>=mid) ans=mid,L=mid+1;
else R=mid-1;
} */
int L=1,R=min(min(x1-x0+1,y1-y0+1),qry(x0,x1,y0,y1)),ans=0;
while(L<=R){
int mid=(L+R)>>1;
// printf("[%d,%d], mid = %d\n",L,R,mid);
if(qry(x0,x1-mid+1,y0,y1-mid+1)>=mid) ans=mid,L=mid+1;
else R=mid-1;
}
printf("%d\n",ans);
}
return 0;
}
T3
题面
现在给你一棵 \(n\) 个节点的树,第 \(i\) 个节点的点权为 \(a_i\)。一个节点可以控制节点 \(v\) 当且仅当 \(u, v\) 间的距离 \(dis(u, v) \leq a_u\)。
对于树上的一个非空点集 \(V\),如果存在一个点 \(x\) 使得点集中的所有的点都可以控制 \(x\),则这个点集的权值为 \(2^{|V|}\),其中 \(|V|\) 表示点集中点的个数;否则这个点集的权值为 \(0\)。
现在请你求出,所有不同的点集的权值之和为多少,由于这个答案可能很大,你只需要输出它对 \(998244353\) 取模后的结果。
Note:两个点集不同,当且仅当点集中至少有一个元素不同。
\(1 \leq n \leq 200000, 1 \leq a_i \leq n\)。
sol
\(x\) 不一定要在 \(V\) 中。
如果某条边的两个端点都被 \(u\) 控制,称这条边被 \(u\) 控制。
考虑求出每个点 \(u\) 能被多少个点控制。若能控制 \(u\) 的点集为 \(S_u\),枚举 \(S_u\) 的每个子集 \(V\),那么点 \(u\) 的贡献为 \(\sum\limits_{V \subseteq S_u, V \neq \varnothing} 2^{|V|} = 3^{|S|} - 1\)。
这样会算重复,因为 \(S_u\) 与 \(S_v\) 可能有相同的子集,需要减去。注意到对于一个集合 \(V\),设 \(T\) 为被 \(V\) 中所有点同时控制的点的集合,那么 \(T\) 中的点构成原树的一个联通块(也是树形的),满足连通块中 边数 = 点数 - 1。按照上面的方法,\(T\) 中的每个点都把集合 \(V\) 的权值贡献了一次。而显然连通块中的每条边都被 \(V\) 控制,所以可以求出每条边被多少个节点控制,最后减去这些集合的贡献即可。
上述过程可以用点分治实现。可以把边 \((u, v)\) 拆成 \((u, x), (x, v)\),并且 \(a_i \gets 2a_i\),这样若 \(x\) 可以被点 \(w\) 控制则边 \((u, v)\) 可以被 \(w\) 控制。
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define int long long
const int M=8e5+10,mod=998244353;
const int inf=1e9+7;
int max(int A,int B){return A>B?A:B;}
int n,ans,a[M],pw3[M];
void pre(){
pw3[0]=1;
for(int i=1;i<M;i++) pw3[i]=3ll*pw3[i-1]%mod;
}
int head[M],cnte;
struct Edge{int to,next;}e[M<<1];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
bool vis[M];
int rt,minsiz,totsiz,siz[M];
void getRoot(int u,int fa){
siz[u]=1; int tmp=0;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v==fa||vis[v]) continue;
getRoot(v,u),siz[u]+=siz[v];
tmp=max(tmp,siz[v]);
}
tmp=max(tmp,totsiz-siz[u]);
if(tmp<minsiz) minsiz=tmp,rt=u;
}
int dep[M];
std::vector<int>ds,sub;
void getdep(int u,int fa){
sub.push_back(u),siz[u]=1;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v==fa||vis[v]) continue;
dep[v]=dep[u]+1,getdep(v,u);
siz[u]+=siz[v];
}
}
int cnt[M];
#define qwq(a,x) lower_bound(a.begin(),a.end(),x)
void calc(int u,int w,bool del){
sub.clear(),ds.clear();
dep[u]=w,getdep(u,0);
for(int v:sub) ds.push_back(a[v]-dep[v]);
std::sort(ds.begin(),ds.end());
for(int v:sub){
int con=ds.end()-qwq(ds,dep[v]);
cnt[v]+=(del)?(-con):(con);
}
}
void solve(int u){
vis[u]=true,calc(u,0,false);
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(vis[v]) continue;
calc(v,1,true);
minsiz=inf,totsiz=siz[v];
getRoot(v,0),solve(rt);
}
}
signed main(){
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
pre();
memset(head,-1,sizeof head);
scanf("%lld",&n);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]),a[i]*=2;
for(int i=1,u,v;i<n;i++){
scanf("%lld%lld",&u,&v);
add(u,i+n),add(i+n,u);
add(v,i+n),add(i+n,v);
}
minsiz=inf,totsiz=n;
getRoot(1,0),solve(rt);
for(int i=1;i<=n;i++) (ans+=pw3[cnt[i]]-1)%=mod;
for(int i=n+1;i<2*n;i++) (ans+=mod+1-pw3[cnt[i]-1])%=mod;
printf("%lld",ans);
return 0;
}
T4
题面
在战火纷飞,饿殍遍野的黑暗时代,jambow 以其怀天下苍生的仁心和他人难以望其项背的实力,统一了 X501-G 大陆,建立起了塔克国。塔克国最高领袖的称号为“塔克”。
岁月更迭,由于地理位置以及其他种种复杂的原因,jambow 决定迁移首都。迁移首都有很多必要工作,而其中有一项便是重新规划地下水道。
jambow 由于这项工作繁杂且小弟们无一能胜任这项工作,所以决定亲自上阵,展现他作为塔克的实力。
虽然在之前 jambow 还没有研究过这类叫做网络流的问题,但以他的实力,\(10^{-16}s\) 内学会当然不在话下。
在学习过程中,jambow 抄了一些笔记:
通常在运筹学中,有向图称为网络,顶点称为节点而边称为弧。
如果带枚有限的有向图 \(G = (V, E)\) 满足如下条件,则称之为网络流图(或容量网络):
- 有且仅有一个节点 \(s \in V\) 入度为 \(0\),称为源点。
2.有且仅有一个节点 \(t \in V\) 出度为 \(0\),称为汇点。
3.\(\forall (u, v) \in E, \exists c(u,v) \in R^+\),称为这条弧的容量。
我们将通过容量网络中一条弧 \((u,v)\) 的流量(或净流)记为 \(f(u, v)\))。如果一个流量的集合 \(F = f(u, v)\) 包含所有弧上的所有流,则称 \(F\) 为这个容量网络的一个网络流。
在任意时刻,G的网络流均满足如下性质。
-
容量限制:\(f(u,v) \leq c(u,v)\)。
-
反对称性:\(\forall u,v \in V, f(u,v) = -f(v, u)\)。
-
流守恒:\(\forall u \in V - \{s,t\}\),有 \(\sum\limits_{v \in V} f(u, v) = 0\)。
对于一个容量网络,其最大流指的是在所有可能的网络流中,汇点收到的流量最大的网络流。
(以上为笔记内容)
jambow 思来想去,在 \(5 \times 10^{-19}s\) 后终于得到了一个 \(O(n+m)\) 的求解最大流的算法。他觉得这个问懸有些过于简单,于是稍微扩充了一下定义:
容量网络:有向图 → 无向图,任意两点都可为源点汇点。
弧:若 \((u, v) \notin E\),则假定存在一条 \((u, v)\) 的弧,且 \(c(u, v) = 0\)。
过了 \(5 \times 10^{-19}s\) 后,jambow 准备将他的知识传授给小弟,于是准备先让他们思考如何求解任意两点最大流这个问题再授课。
jambow 很快就意识到这个问题对他的小弟来说有些困难,于是限制了 \(\forall u \in V, deg_u \leq d, \forall (u, v) \in E, c(u, v) = 1\)(\(deg_u\) 表 \(i\) 点的度数),并且只需要他们回答所有无序点对间最大流的和。
然而 \(\color{grey} \mathrm{EndSaH}\) 依然不会求解这个弱化版的问题,又直接不能去问塔克 jambow,所以只能来问你了。
对于所有数据,满足 \(1 \leq n \leq 3000, 1 \leq m \leq 4500, d \in \{1, 2, 3\}, 1 \leq u, v \leq n, u \neq v\)。
对于前 10% 的数据,\(1 \leq n, m \leq 8\)。
对于前 30% 的数据,\(1 \leq n, m \leq 200\)。
对于另外 20% 的数据,\(d = 2\)。
sol
实际上问你无序点对 \((u, v)\) 的最大流之和。
可以直接上 Gomory-Hu Tree,但是因为度数 \(\leq 3\) 所以最大流最多为 \(3\),可以分类讨论做。
最大流等于最小割,所以考虑 \((u, v)\) 不联通需要切掉几条边(容量为 \(1\)):
-
当 \((u, v)\) 不连通,显然最小割为 \(0\)。
-
当 \((u, v)\) 连通但是不在同一个边双连通分量,最小割为 \(1\)。
-
当 \((u, v)\) 在同一个边双联通分量,但是不在同一个边三连通分量,最小割为 \(2\)。
-
当 \((u, v)\) 在同一个点三连通分量,最小割为 \(3\)。
参考这题的做法,可以把数据出到 \(n, m \leq 5 \times 10^5\),但是因为数据比较小所以可以暴力每次去掉一条边,求出每个点双连通分量。如果存在两个点,不论切掉哪条边都在同一个边双里,说明二者边三连通。
如果暴力枚举 \((u,v)\),每次用 \(O(m)\) 的时间判断二者在同一个三连通分量,时间复杂度为 \(O(n^2 m)\),不可承受。所以考虑对于点 \(u\) 用哈希将「点 \(u\) 在割掉第 \(i\) 条边时属于第 \(x\) 个边双」的信息压缩起来,就可以 \(O(1)\) 判断了。
#include<cstdio>
#include<cstring>
#define gmin(a,b) (a=((a)<(b)?(a):(b)))
const int M=3010,N=4514;
int n,m,d,ans;
bool p1[M][M];
int head[M],cnte=1;
struct Edge{
int to,next; bool mark;
}e[N<<1];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u],false};
head[u]=cnte;
e[++cnte]=(Edge){u,head[v],false};
head[v]=cnte;
}
struct DSU{
int fa[M];
DSU(){for(int i=1;i<M;i++) fa[i]=i;}
int f(int x){return fa[x]==x?x:fa[x]=f(fa[x]);}
void m(int x,int y){fa[f(x)]=f(y);}
bool q(int x,int y){return f(x)==f(y);}
}dsu;
int stk[M],bel[M],dfn[M],low[M];
int idx,col,top,tmp;
void Tarjan(int u,int fa){
stk[++top]=u,low[u]=dfn[u]=++idx;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v==fa||e[i].mark) continue;
if(!dfn[v]) Tarjan(v,u),gmin(low[u],low[v]);
else gmin(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
++col;
do bel[tmp=stk[top--]]=col; while(tmp!=u);
}
}
int bels[M][N]; // bels[i][j] 切掉第 j 条边后 i 所属的双连通分量编号
typedef unsigned long long ULL;
ULL B=1e8+7; ULL H[M];
int main(){
freopen("flow.in","r",stdin);
freopen("flow.out","w",stdout);
memset(head,-1,sizeof head);
scanf("%d%d%d",&n,&m,&d);
for(int i=1,x,y;i<=m;i++)
scanf("%d%d",&x,&y),add(x,y),dsu.m(x,y);
for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i,0);
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
if(bel[i]!=bel[j]&&dsu.q(i,j)) ans++,p1[i][j]=true;
for(int E=1;E<=m;E++){
col=idx=0;
for(int i=1;i<=n;i++) bel[i]=dfn[i]=low[i]=0;
e[E<<1].mark=e[E<<1|1].mark=true;
for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i,0);
for(int i=1;i<=n;i++) bels[i][E]=bel[i];
e[E<<1].mark=e[E<<1|1].mark=false;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) H[i]=H[i]*B+bels[i][j];
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
if(dsu.q(i,j)&&!p1[i][j]) ans+=2+(H[i]==H[j]);
printf("%d",ans);
return 0;
}
2023.7.14
\(10 + 0 + 10 + 0\)
T1
题面
今天是 Bessie 的生日,他买了一个蛋糕和朋友们一起分享,蛋糕可以看成是一个 \(R\) 行 \(C\) 列的表格,共有 \(R \times C\) 个格子,每个格子都有一个 \(0\) 至 \(9\) 的数字,表示该格子蛋糕拥有的巧克力。现在Bessie要把蛋糕横的切 3 刀再竖的切 3 刀,由于 Bessie 刀法厉害,所以每个格子蛋糕都是完整的,显然蛋糕会被切成 16 份,然后 Bessie 和他的 15 个朋友们每人拿一份,Bessie 比较客气,总是等其他朋友拿完了,Bessie 拿最后剩下的那一份。Bessie 的朋友们都很不客气,都是挑最多巧克力的那份去拿,于是 Bessie 最后拿到手的那份蛋糕总是巧克力总和最少的。Bessie 心想:既然自己总是最后拿蛋糕,那应该怎么切蛋糕,才能使得自己拿的那部分蛋糕的有尽量多的巧克力呢?这个问题自然是你的任务了。
40% 的数据,\(4 \leq R,C \leq 10\)。
60% 的数据,\(4 \leq R,C \leq 20\)。
100% 的数据,\(4 \leq R,C \leq 75\)。
sol
预处理出二维前缀和。
二分最少的那块能否达到 \(mid\),然后枚举横着切在哪里,竖着切尽量靠前切,看能否竖切 3 刀(分成 4 份)即可。
#pragma GCC optimize("Ofast")
#include<cstdio>
const int M=80;
int r,c,a[M][M];
int pre[M][M];
int qsum(int x0,int y0,int x1,int y1){
return pre[x1][y1]-pre[x0-1][y1]-pre[x1][y0-1]+pre[x0-1][y0-1];
}
bool check(int v){
// 最小值 >= v
for(int i=1;i<=r-3;i++)
for(int j=i+1;j<=r-2;j++)
for(int k=j+1;k<=r-1;k++){
int cnt=0,pre=0;
for(int l=1;l<=c;l++){
int v1=qsum(1,pre+1,i,l),v2=qsum(i+1,pre+1,j,l);
int v3=qsum(j+1,pre+1,k,l),v4=qsum(k+1,pre+1,r,l);
if(v1>=v&&v2>=v&&v3>=v&&v4>=v){
pre=l,cnt++;
if(cnt>=4) return true;
}
}
}
return false;
}
int main(){
scanf("%d%d",&r,&c);
for(int i=1;i<=r;i++)
for(int j=1;j<=c;j++) scanf("%1d",&a[i][j]);
for(int i=1;i<=r;i++)
for(int j=1;j<=c;j++)
pre[i][j]=pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+a[i][j];
int ans=0;
int L=0,R=pre[r][c];
while(L<=R){
int mid=(L+R)>>1;
if(check(mid)) L=mid+1,ans=mid;
else R=mid-1;
}
printf("%d",ans);
return 0;
}
T2
题面
新入生欢迎庆典马上要开始了,唯的吉太的弦却出了一些问题,在前一段时间换弦的时候弄乱了,但是唯根本不会修,所以只能拜托澪。
具体来说是这样的,一共有 \(2n\) 个节点按顺序排布在一行上面,他们分成了 \(n\) 对,每一对之间有一根弦,澪需要把一段节点取出来检查。
一共会取出 \(m\) 次,每次会选定两个右端点 \(r_1, r_2\),取出来一段节点的同时不能割断任何一根弦,但是无聊的律想确认符合这样条件的左端点的个数,也就说这个左端点和两个右端点形成的区间取出来以后都不会割断弦。
Subtask 1(10 pts),\(n, m \leq 2 \times 10^3\)。
Subtask 2(10 pts),\(n, m \leq 10^5\),对于 \(i \in [1, n]\),\(i\) 一定和 \(2n - i + 1\) 连弦。
Subtask 3(20 pts),\(n, m \leq 10^5\),对于 \(i \in [1, n]\),\(2i - 1\) 一定和 \(2i\) 连弦。
Subtast 4(20 pts),\(n, m \leq 10^5\),所有的 \(r_1 = r_2\)。
Subtask 5(20 pts),\(n, m \leq 10^5\)。
Subtask 6(20 pts),\(n, m \leq 10^6\)。
sol
通过单调栈求出每个右端点 \(r\) 形成合法区间的最右边的左端点 \(l\),并将 \(r\) 与 \(l - 1\) 连边。
具体来说,将点从左到右插入单调栈。当插入左端点时,将左端点以前的区间全部弹出;当插入右端点时,将其对应区间的内部包含的区间弹出,并将交叉的区间合并即可。
最后会形成一颗树(链?),询问实际上就是求 \(lca(r_1, r_2)\) 的深度。注意 \(0\) 号节点也是树上的节点。
树上倍增会 T 掉,而且这题的树分叉较少,所以用树剖。
#include<cstdio>
#include<cstring>
#include<iostream>
const int M=2e6+10;
inline int max(int A,int B){
return A>B?A:B;
}
inline int min(int A,int B){
return A<B?A:B;
}
inline int read(){
int x=0; char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
return x;
}
int p[M],n,m,rt;
int stk[M],l[M],r[M],top;
int head[M],cnte;
struct Edge{int to,next;}e[M];
void add(int u,int v){
// printf("add(%d,%d)\n",u,v);
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int fa[M],son[M],sz[M],d[M],tp[M];
void dfs1(int u,int f){
fa[u]=f,d[u]=d[f]+1,sz[u]=1;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
dfs1(v,u),sz[u]+=sz[v];
if(sz[v]>sz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int t){
tp[u]=t; if(!son[u]) return;
dfs2(son[u],t);
for(int i=head[u];~i;i=e[i].next)
if(e[i].to!=son[u]) dfs2(e[i].to,e[i].to);
}
int LCA(int u,int v){
while(tp[u]!=tp[v]){
// std::cerr<<"u="<<u<<" v="<<v<<'\n';
if(d[tp[u]]>d[tp[v]]) u=fa[tp[u]];
else v=fa[tp[v]];
}
return d[u]<d[v]?u:v;
}
int main(){
freopen("hotchkiss.in","r",stdin);
freopen("hotchkiss.out","w",stdout);
memset(head,-1,sizeof head);
n=read(),m=read(),rt=2*n+1;
for(int i=1;i<=2*n;i++) p[i]=read();
for(int i=1;i<=2*n;i++){
l[i]=min(p[i],i),r[i]=max(p[i],i);
while(stk>0 && l[i]<=stk[top]){
int x=stk[top--];
l[i]=min(l[i],l[x]),r[i]=max(r[i],r[x]);
}
stk[++top]=i;
}
memset(fa,-1,sizeof fa);
for(int i=1;i<=2*n;i++)
if(r[i]==i) add(l[i]-1,i),fa[i]=l[i]-1;
for(int i=0;i<=2*n;i++)
if(fa[i]==-1) add(rt,i),fa[i]=rt;
dfs1(rt,rt+1),dfs2(rt,rt);
for(int i=1,r1,r2;i<=m;i++){
scanf("%d%d",&r1,&r2);
int lca=LCA(r1,r2);
if(lca==rt) printf("0\n");
else printf("%d\n",d[lca]-2);
}
return 0;
}
T3
题面
有个房子摆成一列,从左到右分别标号为 \(1, 2, \cdots, n\),每个房子有自己的高度。
现在一个人想走路,他有 \(k\) 种不同颜色的旗子。他准备从房子 \(l\) 走到 \(r\),也就是依次走到 \(l, l+1, \cdots, r\)。他会选择一些房子并在房顶上插旗子。如果他在一个房子上插了旗子,那么在接下来的时间内他不会在比这个房子低的房子上插旗子。
他还希望他能刚好使用所有颜色的旗子。他想知道他插旗子的方案数。
\(q\) 次询问。
1 l r k表示将 \(h_l, h_{l+1}, \cdots, h_r\) 全部修改为 \(k\)。
2 l r k表示人想从 \(l\) 走到 \(r\),带了 \(k\) 种不同颜色的旗子,询问方案数。
对 100% 数据,有 \(1 \leq n \leq 50000, 1 \leq q \leq 35000, 1 \leq k \leq 5\),保证任意时刻 \(1 \leq h_i \leq 3\)。
sol
注意不是 \(k\) 个旗子,而是 \(k\) 种,每种无限个。
假设现在有一个高度单调不降,长度为 \(s\) 的房子序列,要在上面恰好插 \(s\) 个旗子,而且每种颜色的旗子都要用到,问方案数。
设 \(F_s(i)\) 为插 \(s\) 个旗子,\(k\) 种旗子中恰好有 \(i\) 种不用的方案数,\(G_s(i)\) 为 \(k\) 种旗子中至少有 \(i\) 种不用的方案数。
根据二项式反演,\(F_s(i) = \sum\limits_{x=i}^k (-1)^x C_k^x G_s(x)\),而显然 \(G_s(x) = (k - x)^s\),故 \(F_s(0) = \sum\limits_{x=0}^k (-1)^x C_k^x (k - x)^s\)。设询问区间长度为 \(s\) 的合法路径有 \(p_s\) 个,那么一次询问的答案为:
中括号内的柿子,可以考虑对每一个底数 \(a = k - x\) 建线段树。每个节点上 \(f_{p, q, a}\) 表示从高度为 \(p\) 的地方走到高度为 \(q\) 的地方的 \(\sum\limits_s p_s a^s\)。
合并两个节点的时候,转移柿子是
这样是正确的,因为左右底数相同,指数表示旗子个数。若左半区间存在 \(p'_s a^s\),右半区间存在 \(p''_t a^t\),那么整个区间存在 \(p'_s p''_t a^{s+t}\),表示左边 \(p'_s\) 个用 \(s\) 个旗子的方案和右边 \(p''_t\) 个用 \(t\) 个旗子的方案组合,可以贡献出 \(p'_s p''_t\) 个用 \(s+t\) 个旗子的方案。
还有区间摊平操作。当整个区间高度都为 \(h\),任意地方都可以插旗子。若区间长度为 \(L\),那么 \(f_{h, h, a} = \sum\limits_{s=1}^L C_L^s a^s = (a+1)^L - 1\)。更新 \(f\) 数组并打上懒标记即可。
#include<cstdio>
#include<cstring>
#define int long long
#define memcyo(x,y) memset(x,y,sizeof x)
const int M=5e4+10,mod=1e9+7;
int n,q,h[M];
int pw[7][M],C[7][7];
void pre(){
C[0][0]=1;
for(int i=1;i<=6;i++){
pw[i][0]=1;
for(int j=1;j<M;j++)
pw[i][j]=1ll*pw[i][j-1]*i%mod;
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=C[i-1][j]+C[i-1][j-1];
}
}
struct node{
int l,r,lazy;
int f[4][4][6];
}tr[M<<2];
#define lc(k) (k<<1)
#define rc(k) (k<<1|1)
void tag(int k,int v){
tr[k].lazy=v;
memcyo(tr[k].f,0);
for(int T=0;T<=5;T++)
tr[k].f[v][v][T]=pw[T+1][tr[k].r-tr[k].l+1]-1;
}
void pushdown(int k){
if(!tr[k].lazy) return;
tag(lc(k),tr[k].lazy),tag(rc(k),tr[k].lazy);
tr[k].lazy=0;
}
node merge(node A,node B){
node C;
C.l=A.l,C.r=B.r,C.lazy=0;
memcyo(C.f,0);
for(int T=0;T<=5;T++)
for(int h0=1;h0<=3;h0++)
for(int h3=h0;h3<=3;h3++){
C.f[h0][h3][T]=(A.f[h0][h3][T]+B.f[h0][h3][T])%mod;
for(int h1=h0;h1<=h3;h1++)
for(int h2=h1;h2<=h3;h2++)
(C.f[h0][h3][T]+=1ll*A.f[h0][h1][T]*B.f[h2][h3][T]%mod)%=mod;
}
return C;
}
void build(int k,int l,int r){
tr[k].l=l,tr[k].r=r;
if(l==r){
memcyo(tr[k].f,0);
for(int T=0;T<=5;T++)
tr[k].f[h[l]][h[l]][T]=T;
return;
}
int mid=(l+r)>>1;
build(lc(k),l,mid),build(rc(k),mid+1,r);
tr[k]=merge(tr[lc(k)],tr[rc(k)]);
}
void update(int k,int l,int r,int v){
if(l<=tr[k].l&&tr[k].r<=r){
tag(k,v); return;
}
pushdown(k);
int mid=(tr[k].l+tr[k].r)>>1;
if(l<=mid) update(lc(k),l,r,v);
if(r>mid) update(rc(k),l,r,v);
tr[k]=merge(tr[lc(k)],tr[rc(k)]);
}
node query(int k,int l,int r){
if(l<=tr[k].l&&tr[k].r<=r) return tr[k];
pushdown(k);
int mid=(tr[k].l+tr[k].r)>>1;
if(r<=mid) return query(lc(k),l,r);
else if(l>mid) return query(rc(k),l,r);
else return merge(query(lc(k),l,r),query(rc(k),l,r));
}
signed main(){
freopen("sendpoints.in","r",stdin);
freopen("sendpoints.out","w",stdout);
pre();
scanf("%lld%lld",&n,&q);
for(int i=1;i<=n;i++) scanf("%lld",&h[i]);
build(1,1,n);
for(int i=1,opt,l,r,k;i<=q;i++){
scanf("%lld%lld%lld%lld",&opt,&l,&r,&k);
if(opt==1) update(1,l,r,k);
if(opt==2){
node res=query(1,l,r);
int ans=0;
for(int T=0;T<=k;T++){
int sum=0;
for(int h0=1;h0<=3;h0++)
for(int h1=h0;h1<=3;h1++)
(sum+=res.f[h0][h1][k-T])%=mod;
int qwq=(T&1)?(mod-1):1;
ans=(ans+1ll*qwq*C[k][T]%mod*sum%mod)%mod;
}
printf("%lld\n",ans);
}
}
return 0;
}
T4
题面
沈先生是 X 国 503 城有名的大资本家。
X 国有 \(n\) 个城市,由 \(m\) 条有向道路相连接,第i条路连接着 \(u_i\) 和 \(v_i\)。
沈先生现在在城市 \(1\),准备去城市 \(2\)。
走过一条路需要时间,但是这个时间现在还不知道,只知道时间 \([l_i, r_i]\) 之间。
虽然沈先生并不能确定是哪条道路用时最少,但还是钦定了一条路径。
小 C 认为这条路径钦定的很差,于是就打算告诉沈先生这个路径最短的前缀,使得走这段路一定用时不是最短的。
小 C \(10^{-114514} s\) 就算出来了,但是为了保险,请你也算一下来帮他验证。
Subtask 1 (20 pts)
保证 \(1\) 到 \(2\) 最多有两条路径。
Subtask 2 (20 pts)
\(r_i - l_i \leq 5, 1 \leq n, m \leq 10\)。
Subtask 3 (20 pts)
\(n, m \leq 20\)。
Subtask 4 (20 pts)
\(n \leq 1000, m \leq 2000\)。
Subtask 5 (20 pts)
没有特殊限制。
对于全部数据,\(1 \leq n \leq 10^5, 1 \leq m, p \leq 2 \cdot 10^5, 1 \leq l_i \leq r_i \leq 10^6\)。
sol
二分走前 \(mid\) 条边,考虑什么时候一定不是最短的。
「一定不是最短的」意味着存在一条长度不超过它的路径。可以考虑一次从 \(1\) 开始走,走到的边权取 \(r_i\);一次从 \(mid\) 条边的终点出发,走到的边权取 \(l_i\)。如果前者到 \(2\) 的最短路小于等于后者到 \(2\) 的最短路,那么走前 \(mid\) 条边一定不是最短的。
但是会出现二者走同一条边的情况,根据贪心这条边要么取 \(l_i\) 要么取 \(r_i\)。事实上,当后者先到或者同时到这条边,那么这条边应取 \(l_i\);前者先到的话这条边应取 \(r_i\)(后者可能不经过)。
#include<queue>
#include<cstdio>
#include<cstring>
#define int long long
using namespace std;
const int M=2e5+10,inf=1e18;
int n,m,p,l[M],r[M],ed[M];
int head[M],cnte;
struct Edge{int to,next;}e[M];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int dis[2][M],mark[M];
bool vis[2][M];
typedef pair<int,int> miku;
priority_queue<miku,vector<miku>,greater<miku>>q;
#define qwq make_pair
bool Dijkstra(int mid){
// mark[i] = 0:未经过的边
// mark[i] = 1: 经过了,设置边权为 r[i]
// mark[i] = 2: 经过了,设置边权为 l[i]
for(int i=1;i<=n;i++)
for(int j=0;j<2;j++) dis[j][i]=inf,vis[j][i]=false;
for(int i=1;i<=m;i++) mark[i]=0;
for(int i=1;i<=mid;i++) mark[ed[i]]=2;
dis[0][1]=0; int pre=0;
for(int i=1;i<=mid;i++){
dis[0][e[ed[i]].to]=(pre+=l[ed[i]]*2);
if(i!=mid&&e[ed[i]].to==2) return true;
}
dis[1][1]=1;
q.push(qwq(dis[0][e[ed[mid]].to],e[ed[mid]].to));
q.push(qwq(dis[1][1],1));
while(!q.empty()){
miku u=q.top(); q.pop();
int cur=u.first&1;
if(vis[cur][u.second]) continue;
vis[cur][u.second]=true;
for(int i=head[u.second];~i;i=e[i].next){
int v=e[i].to,w;
if(mark[i]) w=(mark[i]==2)?(2*l[i]):(2*r[i]);
else mark[i]=(cur==1)?1:2,w=(cur==1)?(2*r[i]):(2*l[i]);
if(dis[cur][v]>dis[cur][u.second]+w)
q.push(qwq(dis[cur][v]=dis[cur][u.second]+w,v));
}
}
return dis[0][2]>=dis[1][2];
}
signed main(){
freopen("travel.in","r",stdin);
freopen("travel.out","w",stdout);
memset(head,-1,sizeof head);
scanf("%lld%lld%lld",&n,&m,&p);
for(int i=1,u,v;i<=m;i++){
scanf("%lld%lld%lld%lld",&u,&v,&l[i],&r[i]);
add(u,v);
}
for(int i=1;i<=p;i++) scanf("%lld",&ed[i]);
int ans=0;
for(int L=1,R=p;L<=R;){
int mid=(L+R)>>1;
if(Dijkstra(mid)) ans=mid,R=mid-1;
else L=mid+1;
}
if(ans==0) printf("No Response!");
else printf("%lld",ed[ans]);
return 0;
}
2023.7.15
T1
题面
JY 是一个爱旅游的探险家,也是一名强迫症患者。现在 JY 想要在 C 国进行一次长途旅行,C 国拥有 \(n\) 个城市(编号为 \(0, 1, 2, \cdots, n-1\)),城市之间有 \(m\) 条道路,可能某个城市到自己有一条道路,也有可能两个城市之间有多条道路,通过每条道路都要花费一些时间。JY 从 \(0\) 号城市开始出发,目的地为 \(n - 1\) 号城市。由于 JY 想要好好参观一下 C 国,所以 JY 想要旅行恰好 \(T\) 小时。为了让自己的旅行更有意思,JY 决定不在任何一个时刻停留(走一条到城市自己的路并不算停留)。JY 想知道是否能够花恰好 \(T\) 小时到达 \(n - 1\) 号城市(每个城市可经过多次)。现在这个问题交给了你。
若可以恰好到达输出Possible否则输出Impossible。
多组数据。
对于 30% 的数据,\(T \leq 10000\)。
另有 30% 的数据,\(n \leq 5, m \leq 10\)。
对于全部数据,\(2 \leq n \leq 50, 1 \leq m \leq 100, 1 \leq z \leq 10000, 1 \leq T \leq 10^{18}, Case \leq 5\)。
sol
考虑第一档分,设 \(dis_{i, j}\) 为从点 \(0\) 到达点 \(i\),用时是否可以为 \(j\),跑 SPFA 即可。
现在想办法缩小 T 的范围。具体的可以选择一条边 \((0, y, z)\),然后在这条边上反复横跳,来回 \(k\) 次,再走别的路径。
这样子每条路径的长度都是 \(2k + w\)。将这些路径按 \(w \bmod 2k\) 分类,设 \(dis_{i, j}\) 为从 \(0\) 走到 \(i\),路径长度 \(\equiv j (\bmod \ 2k)\) 的最短路。跑一遍 SPFA 即可。
最后如果 \(dis_{n-1, T \bmod 2k} > T\) 说明无解,反之有解。
#include<queue>
#include<cstdio>
#include<cstring>
#include<utility>
#define int long long
const int M=51,N=20010;
const int inf=1e18+7;
typedef std::pair<int,int> pii;
#define qwq std::make_pair
#define nino first
#define miku second
inline int min(int A,int B){
return A<B?A:B;
}
int Case,n,m,k;
long long T;
int head[M],cnte;
struct Edge{int to,next,dis;}e[N];
void add(int u,int v,int w){
e[++cnte]=(Edge){v,head[u],w};
head[u]=cnte;
}
std::queue<pii>q;
int dis[M][N]; bool inq[M][N];
// dis[i][j]:从 0 走到 i,路程余数为 j 的最短路
void SPFA(){
for(int i=0;i<M;i++)
for(int j=0;j<N;j++) dis[i][j]=inf;
dis[0][0]=0; q.push(qwq(0,0)),inq[0][0]=true;
while(!q.empty()){
pii u=q.front(); q.pop();
inq[u.nino][u.miku]=false;
for(int i=head[u.nino];~i;i=e[i].next){
int v=e[i].to,disv=dis[u.nino][u.miku]+e[i].dis;
if(dis[v][disv%k]>disv){
dis[v][disv%k]=disv;
if(!inq[v][disv%k])
inq[v][disv%k]=true,q.push(qwq(v,disv%k));
}
}
}
}
signed main(){
scanf("%lld",&Case);
while(Case--){
memset(head,-1,sizeof head),cnte=1;
scanf("%lld%lld%lld",&n,&m,&T);
k=inf;
for(int i=1,u,v,w;i<=m;i++){
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w),add(v,u,w);
if(u==0||v==0) k=min(k,w*2);
}
if(k==inf){
printf("Impossible\n");
continue;
}else{
SPFA();
if(dis[n-1][T%k]>T) printf("Impossible\n");
else printf("Possible\n");
}
}
return 0;
}
T2
题面
小 P 最喜欢的数据结构是线段树。线段树是一棵完全二叉树,其每个节点都对应着一个区间 \([l, r]\),根节点对应的区间为 \([1, n]\) 。对于任意一个节点 \([l, r]\),若 \(l = r\),则它是叶子节点。否则,令 \(mid = \left\lfloor \frac{l + r}{2} \right\rfloor\),其左孩子为 \([l, mid]\),右孩子为 \([mid + 1, r]\)。小 P 觉得,线段树结构优美,用途广泛,并且具有优秀的时空复杂度,因此他非常喜欢线段树。
小 P 的好朋友,小 X,对于一般图最大匹配问题的确定性算法很有研究。匹配是指原图的一个边集,满足集合内的任意两条边都没有相同的端点。最大匹配是所有匹配中所选边集大小最大的匹配方案。
一天,小 X 拿出了一棵表示 \([1, n]\) 的线段树,并且告诉小 P,如果小 P 能答出这棵线段树的最大匹配大小和最大匹配方案数,他就将这棵线段树送给小 P。小 P 苦思冥想许久,仍然不会这个问题。你能帮助小 P 得到这棵线段树吗? 由于方案数会很大,请回答方案数对 \(998244353\) 取模的结果。
\(n \leq 10^{18}, T \leq 10^5\)。
sol
树形 DP。设 \(f_{x, 0/1}\) 表示根节点代表区间 \([1, x]\),且根节点不和/和它的儿子匹配时的最大匹配。
\(f_{x, 0} = \max(f_{mid, 0}, f_{mid, 1}) + \max(f_{x - mid, 0}, f_{x - mid, 1})\)
\(f_{x, 1} = \max(f_{mid, 0} + \max(f_{x - mid, 0}, f_{x - mid, 1}), \max(f_{mid, 0}, f_{mid, 1}) + f_{x - mid, 0})\)
事实上,同一层最多有 2 种不同的区间长度,而同种区间长度所建成的线段树同构。所以区间长度最多 \(2 \log n\) 种,直接对长度种类跑 DP 即可。
对于每个 \(f_{x, 0/1}\),按它的转移点求出对应的方案数 \(g_{x, 0/1}\) 即可。
用map复杂度会变成 \(O(T \log^2 n)\),所以使用unordered_map,但是仍然会 T。预处理出区间长度 \(1 \sim 10^6\) 的 DP 值即可。
#include<map>
#include<cstdio>
#include<utility>
#include<unordered_map>
#define qwq make_pair
#define nino first
#define miku second
#define int long long
typedef std::pair<int,int> pii;
const int mod=998244353;
const int M=1e6+10;
inline int max(int A,int B){
return A>B?A:B;
}
pii df[M],dg[M];
int T,n;
void process(pii fl,pii fr,pii gl,pii gr,pii &fu,pii &gu){
fu.nino=max(fl.nino,fl.miku)+max(fr.nino,fr.miku);
fu.miku=1+max(fl.nino+max(fr.nino,fr.miku),fr.nino+max(fl.nino,fl.miku));
gu=std::qwq(0,0);
int tl=0,tr=0;
if(fl.nino>fl.miku) tl=gl.nino;
else if(fl.nino<fl.miku) tl=gl.miku;
else tl=gl.nino+gl.miku;
if(fr.nino>fr.miku) tr=gr.nino;
else if(fr.nino<fr.miku) tr=gr.miku;
else tr=gr.nino+gr.miku;
gu.nino=tl*tr%mod;
int p1=1+fl.nino+max(fr.nino,fr.miku);
int p2=1+fr.nino+max(fl.nino,fl.miku);
if(fu.miku==p1)
gu.miku=(gu.miku+gl.nino*tr%mod)%mod;
if(fu.miku==p2)
gu.miku=(gu.miku+gr.nino*tl%mod)%mod;
}
std::unordered_map<int,pii>f,g;
pii dfs(int u){
if(u<=1000000){g[u]=dg[u];return df[u];}
if(f.count(u)) return f[u];
int l=u/2,r=u-u/2;
pii fl=dfs(l),fr=dfs(r),ff,gg;
process(fl,fr,g[l],g[r],ff,gg);
g[u]=gg,f[u]=ff;
return ff;
}
signed main(){
freopen("match.in","r",stdin);
freopen("match.out","w",stdout);
dg[1]=std::qwq(1,0);
for(int u=2;u<=1000000;u++){
int l=u/2,r=u-u/2;
process(df[l],df[r],dg[l],dg[r],df[u],dg[u]);
}
scanf("%lld",&T);
while(T--){
scanf("%lld",&n);
pii p=dfs(n);
int ans1=0,ans2=0;
if(p.nino>p.miku) ans1=p.nino,ans2=g[n].nino;
else if(p.nino<p.miku) ans1=p.miku,ans2=g[n].miku;
else ans1=p.nino,ans2=(g[n].nino+g[n].miku)%mod;
printf("%lld %lld\n",ans1,ans2);
f.clear(),g.clear();
}
return 0;
}
T3
题面
但是小 P 是一个肥宅,他并不喜欢走路。于是他找小 X 借来了旅行模拟器,开始了他的云上之旅。
小 P 的旅游地图是一棵 \(n\) 个点的树。他的旅行将持续 \(l(l \leq n)\) 天。每天小 P 都会选择一个点 \(x\),并从 \(1\) 号点顺着最短路径走到 \(x\),并且游玩路径上所有在这天之前没有游玩过的所有景点。同时,小 P 的体力有限,他的体力值上限为 \(d\),这意味着他每天最多会走过 \(d\) 条边。而第二天,经过一晚上的休息后,小 P 的体力值会恢复到 \(d\)。小 P 希望,在 \(l\) 天的旅行结束后,他能够游玩尽量多的景点。
而同时,旅行模拟器为了吸引更多的用户,会进行若干次更新,每次更新后所有旅游景点都会发生翻天覆地的变化。因此,小 P 决定进行 \(m\) 次旅行,每次旅行时,他的体力值都会改变。具体来说,小 P 第 \(i\) 次旅行时的体力值上限为 \(d_i\)。现在小 P 想知道每次旅行他最多能够游玩多少景点呢?
\(n \leq 5 \times 10^6, m \leq 2 \times 10^6\)。
sol
一个显然的贪心策略,就是每次走贡献最多的路径。
操作次数达到 \(10^6\) 级别,显然难以在线操作。这道题是用 \(l\) 条祖孙链去覆盖深度 \(\leq d_i\) 的节点,可以考虑用长链剖分优化贪心。
事实上,每次询问的就是前 \(l\) 长的,深度在 \([1, d_i]\) 的长链的长度之和。我们可以按照询问的 \(d\) 降序排序,每次削去树上的最深的一层节点,其余点的长儿子是不变的。维护一个桶,里面装的是目前至少前 \(l\) 长的长链,同时维护桶里面的第 \(l\) 长的链长度以及前 \(l\) 长链长度和;每次删掉一个节点,就把「它到它所在长链的链顶」这条链从桶里删去,然后把「它的父亲到它的长链的链顶」这条链加入桶中即可。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define gmax(A,B) (A=std::max(A,B))
typedef long long miku;
const int N=5e6+10,M=2e6+10;
const miku mod=2148473648;
namespace io {
const int SIZE = 1 << 22 | 1;
char iBuf[SIZE], *iS, *iT, c;
char oBuf[SIZE], *oS = oBuf, *oT = oBuf + SIZE;
#define gc() (iS == iT ? iT = iBuf + fread(iS = iBuf, 1, SIZE, stdin), (iS == iT ? EOF : *iS++) : *iS++)
template<class I> void gi(I &x) {
int f = 1;
for(c = gc(); c < '0' || c > '9'; c = gc())
if(c == '-') f = -1;
for(x = 0; c >= '0' && c <= '9'; c = gc())
x = (x << 3) + (x << 1) + (c & 15);
x *= f;
}
inline void flush() {
fwrite(oBuf, 1, oS - oBuf, stdout);
oS = oBuf;
}
inline void putc(char x) {
*oS++ = x;
if(oS == oT) flush();
}
template<class I> void print(I x) {
if(x < 0) putc('-'), x = -x;
static char qu[55];
char *tmp = qu;
do *tmp++ = (x % 10) ^ '0';
while(x /= 10);
while(tmp-- != qu) putc(*tmp);
}
struct flusher {
~flusher() {
flush();
}
} _;
}
using io :: gi;
using io :: putc;
using io :: print;
miku pw233[M];
void pre(){
pw233[0]=1;
for(int i=1;i<M;i++) pw233[i]=pw233[i-1]*233%mod;
}
int n,m,l;
int head[N],cnte;
struct Edge{int to,next;}e[N];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
struct node{int dep,mxdep,h,id,fa,son,top;}p[N];
struct qry{int d,id;}q[M];
void dfs(){
for(int u=1;u<=n;u++){
if(u!=1) p[u].dep=p[p[u].fa].dep+1;
p[u].mxdep=p[u].dep;
}
for(int u=n;u>=1;u--)
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;;
gmax(p[u].mxdep,p[v].mxdep);
if(p[v].mxdep>p[p[u].son].mxdep) p[u].son=v;
}
for(int u=1;u<=n;u++){
if(u==1) p[u].top=1;
else if(u==p[p[u].fa].son) p[u].top=p[p[u].fa].top;
else p[u].top=u;
}
for(int u=n;u>=1;u--){
if(!p[u].son) p[u].h=1;
else p[u].h=p[p[u].son].h+1;
}
}
bool mark[N];
miku tot,cnt,ans,limh;
int buc[N],rev[N];
signed main(){
freopen("tour.in","r",stdin);
freopen("tour.out","w",stdout);
pre();
memset(head,-1,sizeof head);
gi(n),gi(m),gi(l);
for(int i=1;i<=n;i++) p[i].id=i;
for(int i=2;i<=n;i++) gi(p[i].fa),add(p[i].fa,i);
dfs();
for(int i=1;i<=n;i++){
if(mark[p[i].top]) continue;
mark[p[i].top]=true,buc[p[p[i].top].h]++;
}
for(int i=n;i>=1&&cnt<l;i--){
if(!buc[i]) continue;
limh=i;
if(cnt+buc[i]<=l) (tot+=buc[i]*i%mod)%=mod;
else (tot+=(l-cnt)*i%mod)%=mod;
cnt+=buc[i];
}
std::sort(p+1,p+1+n,[](node A,node B){return A.dep>B.dep;});
for(int i=1;i<=n;i++) rev[p[i].id]=i;
for(int i=1;i<=m;i++) gi(q[i].d),q[i].id=i;
std::sort(q+1,q+1+m,[](qry A,qry B){return A.d>B.d;});
int cur=1;
for(int i=1;i<=m;i++){
while(cur<=n&&p[cur].dep>q[i].d){
int tp=rev[p[cur].top];
if(p[tp].h>limh) tot--;
else if(p[tp].h==limh){
if((--cnt)<l) tot--,cnt+=buc[--limh]+1;
}
buc[p[tp].h]--,buc[--p[tp].h]++;
cur++;
}
(ans+=tot*pw233[q[i].id]%mod)%=mod;
}
printf("%lld",ans);
return 0;
}
T4
咕咕咕
GDKOI 2023 D1T3
2023.7.16
没做
2023.7.19
\(0 + 45 + 20 + 0\)
T1
题面
Venezuela 是一个动荡不安的国家。Venezuela 可以视为一个 \(n\) 个点,\(m\) 条边的无向联通图。这个国家中有多股势力共 \(k\) 支队伍。第 \(i\) 支队伍会从 \(a_i\) 到 \(b_i\) 巡逻。巡逻时,队伍会选择最短路移动,当队伍在一点可以选择多条边走最短路线时,他们会均匀随机地选择一条边前进。当两支不同势力的队伍的行进路线在某点相交时,这个点就会爆发冲突。在这个危险的国家,sukeban games 正在制作 N1RV Ann-A。你作为一名热情的粉丝,不希望他们遭遇危险。现在你想计算出每个点发生冲突的概率。
多组数据。
保证数据合法,\(1 \leq n, k \leq 1000, m \leq 3000, 1 \leq c_i \leq k, T \leq 5\)。
sol
视 \(n, m, k\) 同阶。
先 bfs 跑最短路,求出 \(p_{i, j}\) 为势力 \(j\) 不经过点 \(i\) 的概率。然后 \(p_{i, j} \gets 1 - p_{i, j}\) 变成了势力 \(j\) 至少一支队伍经过点 \(i\) 的概率。
接着对于每个点,设只有 \(j\) 势力经过点 \(i\) 的概率为 \(a_{i, j}\),那么有 \(a_{i, j} = p_{i, j} \prod\limits_{1 \leq x \leq k, c_x \neq j}(1 - p_{i, x})\)。对于点 \(i\) 答案就是 \(1 - \sum\limits_{x=1}^k a_{i, x}\),可以用前缀积后缀积,求一个点的复杂度为 \(O(n)\)。总复杂度 \(O(T n^2)\)。
#include<queue>
#include<cstdio>
#include<cstring>
const int M=1010,mod=998244353;
int T,n,m,k,inv[M];
int head[M],cnte;
struct Edge{int to,next;}e[M<<1];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
bool vis[M];
int dis[2][M],p[M][M],f[M];
// p[i][j]: 势力 j 不经过 i 的概率
std::queue<int>q;
void bfs(int s,int t,int c){
for(int i=1;i<=n;i++)
for(int j=0;j<2;j++) dis[j][i]=-1;
for(int i=1;i<=n;i++) f[i]=0,vis[i]=false;
q.push(s),dis[0][s]=0;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=head[u];~i;i=e[i].next)
if(dis[0][e[i].to]==-1)
dis[0][e[i].to]=dis[0][u]+1,q.push(e[i].to);
}
q.push(t),dis[1][t]=0;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=head[u];~i;i=e[i].next)
if(dis[1][e[i].to]==-1)
dis[1][e[i].to]=dis[1][u]+1,q.push(e[i].to);
}
q.push(s),f[s]=1;
while(!q.empty()){
int u=q.front(); q.pop();
if(vis[u]) continue; vis[u]=true;
p[u][c]=1ll*p[u][c]*(mod+1-f[u])%mod;
int cnt=0;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(dis[0][t]==dis[0][u]+1+dis[1][v]) cnt++;
}
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(dis[0][t]==dis[0][u]+1+dis[1][v]){
f[v]=(f[v]+1ll*f[u]*inv[cnt]%mod)%mod;
q.push(v);
}
}
}
}
int pre[M];
int main(){
freopen("sukeban.in","r",stdin);
freopen("sukeban.out","w",stdout);
inv[1]=1;
for(int i=2;i<M;i++)
inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
memset(head,-1,sizeof head);
scanf("%d",&T);
while(T--){
for(int i=0;i<M;i++)
for(int j=0;j<M;j++) p[i][j]=1;
memset(head,-1,sizeof head),cnte=1;
scanf("%d%d%d",&n,&m,&k);
for(int i=1,u,v;i<=m;i++)
scanf("%d%d",&u,&v),add(u,v),add(v,u);
for(int i=1,c,a,b;i<=k;i++)
scanf("%d%d%d",&c,&a,&b),bfs(a,b,c);
for(int i=1;i<=n;i++)
for(int j=1;j<=k;j++)
p[i][j]=(mod+1-p[i][j])%mod;
for(int i=1;i<=n;i++){
int ans=1; pre[0]=1;
for(int j=1;j<=k;j++){
pre[j]=1ll*pre[j-1]*(mod+1-p[i][j])%mod;
ans=1ll*ans*(mod+1-p[i][j])%mod;
}
ans=(1+mod-ans)%mod; int suf=1;
for(int j=k;j>=1;j--){
ans=(ans+mod-1ll*pre[j-1]*p[i][j]%mod*suf%mod)%mod;
suf=1ll*suf*(1+mod-p[i][j])%mod;
}
printf("%d\n",ans);
}
}
return 0;
}
T2
题面
某国有 \(n\) 座城市,这些城市之间通过 \(m\) 条单向道路相连,已知每条道路的长度。
不过,小 X 只对其中 \(k\) 座城市感兴趣。
为了更好地规划模拟旅行路线,提升模拟旅行的体验,小X想要知道他感兴趣的城市之间两两最短路的最小值(即在他感兴趣的城市中,最近的一对的最短距离)。
作为一个肥宅,小 X 根本懒得写程序来解决这道问题,于是他把这个问题丢给了你。
\(n \leq 3 \times 10^5, m \leq 10^6\)。
sol
省选原题的加强版,卡掉了 2 只 \(\log\) 的做法。
建出原图和反图,分别求出对于每个节点「到哪个关键点最近」和「从哪个关键点来最近」。设前者为 \(A_u\),后者为 \(B_u\)。若原图存在一条边 \((u, v, w)\),且 \(B_u \neq A_v\) 则将 \(dis(B_u, u) + w + dis(v, A_v)\) 与答案取最小值即可。
#include<queue>
#include<cstdio>
#include<cstring>
const int M=3e5+10,N=1e6+10;
typedef long long MIKU;
const MIKU inf=1e18;
int n,m,k,p[M];
struct Graph{
int head[M],cnte;
int to[N],next[N],dis[N];
void add(int u,int v,int w){
to[++cnte]=v,next[cnte]=head[u];
dis[cnte]=w,head[u]=cnte;
}
Graph(){
memset(head,-1,sizeof head);
cnte=1;
}
}G[2];
struct Edge{int u,v,w;}e[N];
typedef std::pair<MIKU,int> pmiku;
std::priority_queue<pmiku,std::vector<pmiku>,std::greater<pmiku>>q;
MIKU dis[2][M]; bool vis[2][M]; int col[2][M];
#define qwq std::make_pair
void Dijkstra(int T){
for(int i=1;i<=n;i++)
dis[T][i]=inf,vis[T][i]=false,col[T][i]=0;
for(int i=1;i<=k;i++){
q.push(qwq(dis[T][p[i]]=0,p[i]));
col[T][p[i]]=i;
}
while(!q.empty()){
int u=q.top().second; q.pop();
if(vis[T][u]) continue; vis[T][u]=true;
for(int i=G[T].head[u];~i;i=G[T].next[i]){
int v=G[T].to[i];
if(dis[T][v]>dis[T][u]+G[T].dis[i]){
dis[T][v]=dis[T][u]+G[T].dis[i];
q.push(qwq(dis[T][v],v));
col[T][v]=col[T][u];
}
}
}
}
int main(){
freopen("tour.in","r",stdin);
freopen("tour.out","w",stdout);
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
if(e[i].u==e[i].v) continue;
G[0].add(e[i].u,e[i].v,e[i].w);
G[1].add(e[i].v,e[i].u,e[i].w);
}
for(int i=1;i<=k;i++) scanf("%d",&p[i]);
Dijkstra(0),Dijkstra(1);
MIKU ans=inf;
for(int i=1;i<=m;i++)
if(col[0][e[i].u]&&col[1][e[i].v])
if(col[0][e[i].u]!=col[1][e[i].v])
ans=std::min(ans,dis[0][e[i].u]+e[i].w+dis[1][e[i].v]);
printf("%lld",ans);
return 0;
}
T3
题面
10s, 512MB.
众所周知,在一场考试中,三道题是相对独立的,这是因为题目考察的知识点往往不同,不同的知识点往往又具有一定独立性。因此解决一道题目与掌握这道题目涉及的算法知识密切相关。
小 L 想知道他最应该学习什么,小 L 的请求用我们所学的 OI 模型来描述大概是这样的:
-
将比赛看作一棵 \(n\) 个点的树形结构,每个点代表一场比赛且具有三个数
\((a_{i, 1}, a_{i, 2}, a_{i, 3})\),这对应这场比赛中三道题涉及的知识点; -
小 L 的竞赛历程可以视作树上的一条路径 \((u,v)\),你需要求出小 L 最应该学习什么知识,即这条路径上出现次数最多的数是什么(一个点上如果有相同的数算作出现多次);
-
由于世界线的变动,小 L 的竞赛历程可能有微小的变化,因此有多次询问,这里用 \(q\) 表示询问次数。
请你完成小 L 的请求。
对于 50% 的数据,不强制在线,数据较为随机且具有一定梯度。
对于另外 50% 的数据,强制在线。
对于 100% 的数据,\(1 \leq n \leq 8 \times 10^4, 1 \leq q \leq 10^5, 1 \leq a_{i, 1}, a_{i, 2}, a_{i, 3} \leq n\)。
sol
区间众数上树,考虑树分块。
在树上取 \(O(\sqrt{n})\) 个关键点,相邻两个关键点的距离期望 \(O(\sqrt{n})\)。可以按照一般树分块的套路,如果一个子树的大小超过阈值 \(S\) 就将它看作一个块,并从树上切掉。另一种方法是每个点都有 \(\frac{1}{S}\) 的概率成为关键点。
然后把所有关键点两两的 LCA 以及节点 \(1\) 都作为关键点。事实上这样关键点数量级别仍然是 \(O(\sqrt{n})\) 的。用一遍 dfs 求出一个点的祖先中离他最近的关键点以及祖先中关键点的个数。在用 \(O(\sqrt{n})\) 遍 dfs,每次钦定一个关键点为 dfs 起点,求出它和其他任意关键点之间的答案。
查询的时候,分几种情况:
- \((u, v)\) 路径上没有关键点/只有一个关键点。
两点之间距离期望为 \(O(\sqrt{n})\),暴力即可。
- \((u, lca)\) 路径上有关键点,\((v, lca)\) 上没有。
找到 \((u, lca)\) 上距离 \(u\) 最近的关键点 \(u'\) 和距离 \(u\) 最远的的关键点 \(v'\),\((u', v')\) 被预处理了,暴力 \((u, u')\) 和 \((v, v')\) 即可。
- \((v, lca)\) 路径上没有关键点,\((u, lca)\) 上有。
同 2.
- 两段路径都有关键点。
找到离 \(u\) 最近的关键点 \(u'\),离 \(v\) 最近的关键点 \(v'\),\((u', v')\),\((u, u')\) 和 \((v, v')\) 分别暴力即可。
找到最近的关键点是预处理的,最远的关键点可以树上倍增找,搭配上预处理的祖先中「关键点的个数」即可。
可以在线化莫队。
#pragma GCC optimize("Ofast")
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
const int M=8e4+10,S=300;
const int N=M/S*2;
int Type,n,q,xorsum,a[M][3];
int head[M],cnte;
struct Edge{int to,next;}e[M<<1];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int sz0[M];
std::vector<int>key;
int fa[M],son[M],sz[M],dep[M];
int anc[M][18],dfn[M],idx;
void dfs1(int u,int f){
dfn[u]=++idx;
fa[u]=f,dep[u]=dep[f]+1;
sz[u]=sz0[u]=1;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v==f) continue;
dfs1(v,u);
sz[u]+=sz[v],sz0[u]+=sz0[v];
if(sz[v]>sz[son[u]]) son[u]=v;
}
if(sz0[u]>=S) key.push_back(u),sz0[u]=0;
}
int top[M];
void dfs2(int u,int t){
top[u]=t;
if(!son[u]) return;
dfs2(son[u],t);
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v!=fa[u]&&v!=son[u]) dfs2(v,v);
}
}
int LCA(int u,int v){
while(top[u]!=top[v])
if(dep[top[u]]>dep[top[v]]) u=fa[top[u]];
else v=fa[top[v]];
return dep[u]<dep[v]?u:v;
}
bool isKey[M];
int keyid[M];
int bc[M],keyfa[M],keycnt[M];
int cnt[N][M];
void dfs3(int u,int lastkey){
for(int j=0;j<3;j++) bc[a[u][j]]++;
if(isKey[u]){
lastkey=u;
int ID=keyid[u];
memcpy(cnt[ID],bc,sizeof bc);
}
keyfa[u]=lastkey;
keycnt[u]=keycnt[fa[u]]+isKey[u];
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v==fa[u]) continue;
dfs3(v,lastkey);
}
for(int j=0;j<3;j++) bc[a[u][j]]--;
}
struct Ans{
int col,cnt;
Ans(int _col=0,int _cnt=0):col(_col),cnt(_cnt){}
bool operator<(const Ans &o)const{
return cnt<o.cnt||cnt==o.cnt&&col>o.col;
}
bool operator>(const Ans &o)const{
return cnt>o.cnt||cnt==o.cnt&&col<o.col;
}
};
Ans ans[N][N],buc[M],mx;
void dfs4(int u,int f,int rt){
Ans lastver=mx;
for(int j=0;j<3;j++){
buc[a[u][j]].cnt++;
if(mx<buc[a[u][j]]) mx=buc[a[u][j]];
}
if(isKey[u]) ans[rt][keyid[u]]=mx;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(v==f) continue;
dfs4(v,u,rt);
}
for(int j=0;j<3;j++) buc[a[u][j]].cnt--; // backtrack
mx=lastver; // backtrack
}
Ans solve(int u,int v){
int lca=LCA(u,v);
Ans ret;
for(int w=u;w!=lca;w=fa[w])
for(int j=0;j<3;j++){
buc[a[w][j]].cnt++;
if(buc[a[w][j]]>ret) ret=buc[a[w][j]];
}
for(int w=v;w!=lca;w=fa[w])
for(int j=0;j<3;j++){
buc[a[w][j]].cnt++;
if(buc[a[w][j]]>ret) ret=buc[a[w][j]];
}
for(int j=0;j<3;j++){
buc[a[lca][j]].cnt++;
if(buc[a[lca][j]]>ret) ret=buc[a[lca][j]];
}
for(int j=0;j<3;j++) buc[a[lca][j]].cnt--;
for(int w=v;w!=lca;w=fa[w])
for(int j=0;j<3;j++) buc[a[w][j]].cnt--;
for(int w=u;w!=lca;w=fa[w])
for(int j=0;j<3;j++) buc[a[w][j]].cnt--;
return ret;
}
int jump(int u,int lca){
for(int i=17;i>=0;i--)
if(keycnt[anc[u][i]]-keycnt[lca]>0) u=anc[u][i];
return u;
}
int cntval(int u,int v,int w){
int lca=LCA(u,v);
int ret=cnt[keyid[u]][w]+cnt[keyid[v]][w]
-cnt[keyid[lca]][w]*2;
for(int j=0;j<3;j++) ret+=(a[lca][j]==w);
return ret;
}
std::vector<int>path;
int main(){
freopen("mode.in","r",stdin);
freopen("mode.out","w",stdout);
memset(head,-1,sizeof head);
scanf("%d",&Type);
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=0;j<3;j++) scanf("%d",&a[i][j]);
for(int i=1,x,y;i<n;i++)
scanf("%d%d",&x,&y),add(x,y),add(y,x);
dfs1(1,0),dfs2(1,1);
for(int u=1;u<=n;u++) anc[u][0]=fa[u];
for(int i=1;i<18;i++)
for(int u=1;u<=n;u++)
anc[u][i]=anc[anc[u][i-1]][i-1];
for(int p:key) isKey[p]=true;
if(!isKey[1]) isKey[1]=true,key.push_back(1);
for(int p0:key)
for(int p1:key){
if(p1<=p0) continue;
int lca=LCA(p0,p1);
if(!isKey[lca])
isKey[lca]=true,key.push_back(lca);
}
for(int i=0;i<key.size();i++) keyid[key[i]]=i;
dfs3(1,1);
for(int i=1;i<=n;i++) buc[i].col=i;
for(int p:key) dfs4(p,0,keyid[p]);
scanf("%d",&q);
while(q--){
int u,v,lca; Ans res;
scanf("%d%d",&u,&v);
if(Type==1) u^=xorsum,v^=xorsum;
lca=LCA(u,v);
int KEYCNT=keycnt[u]+keycnt[v]-keycnt[lca]*2+isKey[lca];
if(KEYCNT<=1) res=solve(u,v);
else{
if(dfn[u]>dfn[v]) std::swap(u,v);
bool flagu=false,flagv=false;
int keyu=0,keyv=0;
if(keycnt[u]-keycnt[lca]>0) keyu=keyfa[u];
else keyu=jump(v,lca),flagu=true;
if(keycnt[v]-keycnt[lca]>0) keyv=keyfa[v];
else keyv=jump(u,lca),flagv=true;
res=ans[keyid[keyu]][keyid[keyv]];
for(int w=u;w!=lca&&w!=keyu;w=fa[w]){
path.push_back(w);
for(int j=0;j<3;j++){
buc[a[w][j]].cnt++;
Ans qwq=buc[a[w][j]];
qwq.cnt+=cntval(keyu,keyv,a[w][j]);
if(qwq>res) res=qwq;
}
}
for(int w=v;w!=lca&&w!=keyv;w=fa[w]){
path.push_back(w);
for(int j=0;j<3;j++){
buc[a[w][j]].cnt++;
Ans qwq=buc[a[w][j]];
qwq.cnt+=cntval(keyu,keyv,a[w][j]);
if(qwq>res) res=qwq;
}
}
if(flagu)
for(int w=fa[keyu];w!=fa[lca];w=fa[w]){
path.push_back(w);
for(int j=0;j<3;j++){
buc[a[w][j]].cnt++;
Ans qwq=buc[a[w][j]];
qwq.cnt+=cntval(keyu,keyv,a[w][j]);
if(qwq>res) res=qwq;
}
}
if(flagv)
for(int w=fa[keyv];w!=fa[lca];w=fa[w]){
path.push_back(w);
for(int j=0;j<3;j++){
buc[a[w][j]].cnt++;
Ans qwq=buc[a[w][j]];
qwq.cnt+=cntval(keyu,keyv,a[w][j]);
if(qwq>res) res=qwq;
}
}
for(int w:path)
for(int j=0;j<3;j++) buc[a[w][j]].cnt--;
path.clear();
}
xorsum^=res.cnt^res.col;
printf("%d %d\n",res.cnt,res.col);
}
return 0;
}
T4
咕咕咕
2023.7.20
\(0 + 55 + 0 + 100\)
T1
题面
mrsrz 是个珂学家,她正在进行她的珂研项目。
这天,mrsrz 渴了,想喝饮料。她突然想起自己还有一些化学试剂,所以打算自己配饮料。
mrsrz 找到了 \(n\) 种不同的试剂。由于 mrsrz 很粗心,没有给试剂贴标签,所以她并不知道每种试剂的成分是什么。因此,mrsrz 将这些试剂分别编号为 \(1 \sim n\)。mrsrz 打算从其中选两种不同的试剂配饮料.为了配出可口的饮料,mrsrz 需要知道每种试剂的可口度。mrsrz 把所有试剂都尝了一口,得出了试剂的可口度为 \(v_i\)。
化学反应是非常危险的,为了保证安全,mrsrz 给每种试剂规定了用量范围。试剂的用量可以是 \(l_i \sim r_i\) 的任意整数。两种试剂反应后的总量是两种试剂的用量之和,而反应后的试剂的可口度,为两种试剂的可口度的乘积。
现在,mrsrz 想知道,所有配出来总量为 \(k\) 的不同饮料的可口度之和是多少。
两种饮料不同当且仅当配成两种饮料的试剂中有一者或两者不同,或两种试剂的用量不同。
mrsrz 会问 \(m\) 个这样的问题,你只需要对每个问题,告诉她答案对 \(998,244,353\) 取模后的结果即可。
对于 100% 的数据,\(1 \leq n \leq 5000, 1 \leq m \leq 5 \times 10^5, 1 \leq v_i \leq 998244352\)
\(1 \leq l_i, r_i \leq 10^7, 1 \leq k_i \leq 2 \times 10^7\)。
sol
\(m, n, k\) 的范围实际上暗示你 \(O(n^2)\) 预处理 \(O(1)\) 查询,可以考虑枚举两瓶试剂,计算它们对答案的贡献。
设枚举到 \(x, y\) 试剂,可以写出一段暴力代码:
for(int i=l[x];i<=r[x];i++)
for(int j=l[y];j<=r[y];j++) ans[i+j]+=v[x]*v[y];
这样总复杂度是 \(O(n^2 V^2)\) 的,考虑优化。事实上在 \(i\) 固定时,相当于对 \([i + l_y, i + r_y]\) 区间加 \(v_x v_y\)。考虑差分优化掉内层循环:
for(int i=l[x];i<=r[x];i++){
dif[i+l[y]]+=v[x]*v[y];
dif[i+r[y]+1]-=v[x]*v[y];
}
这样总复杂度为 \(O(n^2 V)\),仍然不可接受。观察到上述代码在对 \(dif\) 的 \([l_x + l_y, r_x + l_y]\) 区间加 \(v_x v_y\),\([l_x + r_y + 1, r_x + r_y + 1]\) 区间减 \(v_x v_y\),可以对 \(dif\) 数组进行差分,就可以再次去掉一层循环。
diff[l[i]+l[j]]+=v[i]*v[j];
diff[r[i]+l[j]+1]-=v[i]*v[j];
diff[l[i]+r[j]+1]-=v[i]*v[j];
diff[r[i]+r[j]+2]+=v[i]*v[j];
这样复杂度是 \(O(n^2)\),可以接受。最后将 \(diff\) 数组前缀和可以得到 \(dif\) 数组,而 \(dif\) 数组前缀和得到 \(ans\) 数组,即可 \(O(1)\) 查询。
#include<cstdio>
#define int long long
const int M=5010,N=2e7+10;
const int mod=998244353;
int n,m,v[M],l[M],r[M];
int diff[N];
signed main(){
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld%lld%lld",&v[i],&l[i],&r[i]);
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++){
(diff[l[i]+l[j]]+=1ll*v[i]*v[j]%mod)%=mod;
(diff[r[i]+l[j]+1]-=1ll*v[i]*v[j]%mod-mod)%=mod;
(diff[l[i]+r[j]+1]-=1ll*v[i]*v[j]%mod-mod)%=mod;
(diff[r[i]+r[j]+2]+=1ll*v[i]*v[j]%mod)%=mod;
}
for(int i=1;i<N;i++)
(diff[i]+=diff[i-1])%=mod;
for(int i=1;i<N;i++)
(diff[i]+=diff[i-1])%=mod;
for(int i=1,k;i<=m;i++){
scanf("%lld",&k);
printf("%lld\n",diff[k]);
}
return 0;
}
T2
题面
现在有一张 \(n\) 个点,\(m\) 条边的带权连通图,边权非负。对每一条边,计算有多少条经过这条边的从 \(1\) 到 \(n\) 的最短路。
答案对 \(10^7 + 7\) 取模,如果存在无穷多条经过这条边的最短路,输出inf。
sol
数据出锅,还没有标明边权范围,也没有说清楚是无向边
边权非负所以只有 \(0\) 和正数。如果整张图只有正数边权就不可能出现inf。
考虑用并查集将边权为 \(0\) 的边相连的点缩成一个,因为这些点的最短路都是一样的。将这些点打上标记。
分别从源点和汇点跑一遍最短路,记作 \(dis_{0, u}\) 和 \(dis_{1, u}\),并同时记录最短路个数 \(f_{0/1, u}\) 以及是否有可能经过带标记的点。
跑出来 \(1 \to n\) 的最短路 \(D\) 然后对于每条边 \((u, v, w)\),分类讨论一手:
- \(w = 0\)
此时 \(u, v\) 必然被缩成一个点 \(f\)。若 \(dis_{0, f} + dis_{1, f} = D\) 则答案为inf,否则为 \(0\)。
- \(w \neq 0\)
这时 \(u, v\) 不一定在同一个集合。于是又分为如下几种情况。
- \(1 \to u \to v \to n\) 为最短路
若 \(1 \to u\) 最短路可能经过标记点,或者 \(v \to n\) 最短路可能经过标记点,那么答案为inf。否则答案为 \(f_{0, u} f_{1, v}\)。
- \(1 \to v \to u \to n\) 为最短路
若 \(1 \to v\) 最短路可能经过标记点,或者 \(u \to n\) 最短路可能经过标记点,那么答案为inf。否则答案为 \(f_{0, v} f_{1, u}\)。
- \(1 \to u \to v \to n\) 和 \(1 \to v \to u \to n\) 都是最短路
若 \(1 \to u/v\) 或 \(u/v \to n\) 的最短路任意一个可能经过标记点,那么答案为inf。否则答案为 \(f_{0, u} f_{1, v} + f_{0, v} f_{1, u}\)。
#include<queue>
#include<cstdio>
#include<utility>
#include<cstring>
#define int long long
const int M=2023,INF=1e18+7;
const int mod=1e7+7;
int n,m;
struct node{int u,v,w;}g[M];
int fa[M];
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y){
fa[find(x)]=find(y);
}
bool qry(int x,int y){
return find(x)==find(y);
}
bool zero[M];
int head[M],cnte;
struct Edge{int to,next,dis;}e[M<<1];
void add(int u,int v,int w){
e[++cnte]=(Edge){v,head[u],w};
head[u]=cnte;
}
bool vis[2][M],passzero[2][M];
int dis[2][M],f[2][M];
typedef std::pair<int,int> pii;
std::priority_queue<pii,std::vector<pii>,std::greater<pii>>q;
#define qwq std::make_pair
void Dijkstra(int T,int s){
for(int i=1;i<=n;i++) dis[T][i]=INF;
q.push(qwq(dis[T][s]=0,s)),f[T][s]=1;
passzero[T][s]|=zero[s];
while(!q.empty()){
int u=q.top().second; q.pop();
if(vis[T][u]) continue; vis[T][u]=true;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(dis[T][v]>dis[T][u]+e[i].dis){
dis[T][v]=dis[T][u]+e[i].dis,f[T][v]=f[T][u];
passzero[T][v]=passzero[T][u]|zero[v];
q.push(qwq(dis[T][v],v));
}else
if(dis[T][v]==dis[T][u]+e[i].dis){
(f[T][v]+=f[T][u])%=mod;
passzero[T][v]|=passzero[T][u]|zero[v];
}
}
}
}
signed main(){
freopen("impossible.in","r",stdin);
freopen("impossible.out","w",stdout);
for(int i=0;i<M;i++) fa[i]=i;
memset(head,-1,sizeof head);
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++){
int tot=scanf("%lld%lld%lld",&g[i].u,&g[i].v,&g[i].w);
if(tot<3) g[i]=g[i-1];
if(g[i].w==0) merge(g[i].u,g[i].v),zero[find(g[i].u)]=true;
}
for(int i=1;i<=m;i++)
add(find(g[i].u),find(g[i].v),g[i].w),
add(find(g[i].v),find(g[i].u),g[i].w);
Dijkstra(0,find(1)),Dijkstra(1,find(n));
int mndis=dis[0][find(n)];
for(int i=1;i<=m;i++){
int u=g[i].u,v=g[i].v;
if(qry(u,v)){
int f=find(u);
if(dis[0][f]+dis[1][f]==mndis) printf("inf\n");
else printf("0\n");
}else{
u=find(u),v=find(v);
int p1=dis[0][u]+g[i].w+dis[1][v];
int p2=dis[0][v]+g[i].w+dis[1][u];
if(p1!=mndis&&p2!=mndis) printf("0\n");
else if(p2!=mndis){
if(passzero[0][u]||passzero[1][v]) printf("inf\n");
else printf("%lld\n",1ll*f[0][u]*f[1][v]%mod);
}else if(p1!=mndis){
if(passzero[0][v]||passzero[1][u]) printf("inf\n");
else printf("%lld\n",1ll*f[0][v]*f[1][u]%mod);
}else{
if(passzero[0][u]||passzero[1][v]) printf("inf\n");
else if(passzero[0][v]||passzero[1][u]) printf("inf\n");
else printf("%lld\n",(1ll*f[0][u]*f[1][v]%mod+1ll*f[0][v]*f[1][u]%mod)%mod);
}
}
}
return 0;
}
事实上不缩点也行,只要跑最短路的时候记录是否可能经过 \(0\) 权边即可。
#include<queue>
#include<cstdio>
#include<cstring>
#define int long long
const int M=2023,INF=1e18+7;
const int mod=1e7+7;
int n,m;
struct node{int u,v,w;}g[M];
int head[M],cnte;
struct Edge{int to,next,dis;}e[M<<1];
void add(int u,int v,int w){
e[++cnte]=(Edge){v,head[u],w};
head[u]=cnte;
}
typedef std::pair<int,int> pii;
std::priority_queue<pii,std::vector<pii>,std::greater<pii>>q;
#define qwq std::make_pair
bool passzero[2][M],vis[2][M];
int dis[2][M],f[2][M];
void Dijkstra(int T,int s){
for(int i=1;i<=n;i++) dis[T][i]=INF;
q.push(qwq(dis[T][s]=0,s)),f[T][s]=1;
while(!q.empty()){
int u=q.top().second; q.pop();
if(vis[T][u]) continue; vis[T][u]=true;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(dis[T][v]>dis[T][u]+e[i].dis){
dis[T][v]=dis[T][u]+e[i].dis;
passzero[T][v]=passzero[T][u]|(e[i].dis==0);
f[T][v]=f[T][u];
q.push(qwq(dis[T][v],v));
}else
if(dis[T][v]==dis[T][u]+e[i].dis){
passzero[T][v]|=passzero[T][u]|(e[i].dis==0);
(f[T][v]+=f[T][u])%=mod;
}
}
}
}
signed main(){
freopen("impossible.in","r",stdin);
freopen("impossible.out","w",stdout);
memset(head,-1,sizeof head);
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++){
int tot=scanf("%lld%lld%lld",&g[i].u,&g[i].v,&g[i].w);
if(tot!=3) g[i]=g[i-1];
add(g[i].u,g[i].v,g[i].w),add(g[i].v,g[i].u,g[i].w);
}
Dijkstra(0,1),Dijkstra(1,n);
int mn=dis[0][n];
for(int i=1;i<=m;i++){
int u=g[i].u,v=g[i].v;
int p1=dis[0][u]+g[i].w+dis[1][v];
int p2=dis[0][v]+g[i].w+dis[1][u];
if(p1!=mn&&p2!=mn) printf("0\n");
else if(p2!=mn){
if(passzero[0][u]||passzero[1][v]) printf("inf\n");
else printf("%lld\n",1ll*f[0][u]*f[1][v]%mod);
}else if(p1!=mn){
if(passzero[0][v]||passzero[1][u]) printf("inf\n");
else printf("%lld\n",1ll*f[0][v]*f[1][u]%mod);
}else{
if(passzero[0][u]||passzero[1][v]) printf("inf\n");
else if(passzero[0][v]||passzero[1][u]) printf("inf\n");
else printf("%lld\n",(1ll*f[0][u]*f[1][v]%mod+1ll*f[0][v]*f[1][u]%mod)%mod);
}
}
return 0;
}
T3
题面
xpp 和他的毒瘤朋友在玩游戏。他们两个都是有点聪明。
每次系统会随机出来一个 \(1\) 至 \(m\) 的整数。每一个整数有一个权值 \(w_i\),\(i\) 这个数被随机出来的概率为 \(\frac{w_i}{\sum_{j = 1}^m w_j}\)。
一开始 xpp 拥有选择权,每轮游戏中,拥有选择权的选手可以选择:
1.将随机出来的数加入自己的得分,然后把选择权交给对手。
2.保留选择权,然后将随机出来的数加给对手。
双方的目标都是让自己的分数减去对手的分数最大。他们会一直进行这个游戏。
由于 xpp 和他的毒瘤朋友有点聪明,所以他们是这么考虑的。假设最后拥有选择权的人比对手得分多 \(d\),当随机出来一个整数时,他们会比较选择这个数还是不选择这个数优秀。这样抉择后,拥有决策权的人期望恰好比对手得分多 \(d\)。
(如果随机出来的数i比d大,那么采用第一种操作,如果比d小,那么采用第二种操作,如果相等那么都可能)
i.e.,假如当前随机出来,若 \(i < d\),则期望得分差变为 \(d\),否则,选择权交给对手,期望得分差变为 \(i-d\)。由于他们都有点聪明,当自己操作时,他们都会按照可以使得 \(d\) 最大的方法操作。然后他们会来询问你 \(d\)。
(也就求在经过无数轮后,xpp 期望比对手多多少分?)
有时候 xpp 还会修改一个 \(w_i\),然后继续问你这个问题相信聪明的你一定能帮助 xpp。
注意: 每次修改不是独立的
\(3 \leq m \leq 10^6, 1 \leq Q \leq 10^6, 1 \leq w_i, y_i \leq 100,1 \leq x_i \leq m\)。
sol
设最终答案为 \(a\),如果随机出来一个数 \(i\),那么答案会变成 \(|a - i|\)。
由于随机 「无数」 轮和随机 「无数 \(+ 1\)」 轮答案是一样的,所以有 \(a = \sum\limits_{i = 1}^m \frac{w_i |a - i|}{\sum_{j = 1}^i w_j}\)。
令 \(S = \sum_{j = 1}^m w_j, D = \left\lfloor a \right\rfloor\),那么 \(\sum\limits_{i = 1}^D w_i(a - i) + \sum\limits_{i = D + 1}^m w_i(i - a) - S \cdot a = 0\)。
故考虑先找到整数 \(D\)。
令 \(f(x) = \sum\limits_{i = 1}^{\left\lfloor x \right\rfloor} w_i(x - i) + \sum\limits_{i = \left\lfloor x \right\rfloor + 1}^m w_i(i - x) - S \cdot x\),注意到当 \(x \in N^+, x \in [1, m]\) 时 \(f(x)\) 单调不升,所以可以二分找到 \(f(d) \ge 0 \land f(d + 1) < 0\)。
这时 \(D\) 被确定,直接解一元一次方程即可。
然后要支持单点修改。\(x \in N^+\) 时 \(f(x) = x \sum\limits_{i = 1}^x w_i - \sum\limits_{i = 1}^x i \cdot w_i + \sum\limits_{i = x + 1}^m i \cdot w_i - x \sum\limits_{i = x + 1}^m w_i - S \cdot x\)。直接两颗树状数组,一颗维护 \(\sum w_i\),一颗维护 \(\sum i \cdot w_i\)。
#include<cstdio>
#include<iostream>
#define int long long
const int M=1e6+10,mod=998244353;
inline int qpow(int b,int p){
int ans=1;
for(;p;p>>=1){
if(p&1) ans=ans*b%mod;
b=b*b%mod;
}
return ans;
}
inline int inv(int v){
return qpow(v,mod-2);
}
int m,q,w[M];
#define lb(x) (x&-x)
struct BIT{
int c[M];
void add(int x,int v){
for(;x<=m;x+=lb(x)) c[x]+=v;
}
int qry(int x){
int res=0;
for(;x;x-=lb(x)) res+=c[x];
return res;
}
int qry(int l,int r){
return qry(r)-qry(l-1);
}
}t[2];
int f(int d){
/*
int S=t[0].qry(1,m);
int A=t[0].qry(1,d)-t[0].qry(d+1,m);
int B=t[1].qry(1,d)-t[1].qry(d+1,m);
return A*d-S*d-B;*/
int S=t[0].qry(1,m);
return t[1].qry(1,m)-t[1].qry(1,d)*2+d*(t[0].qry(1,d)*2-S)-S*d;
}
int find_d(){
int L=1,R=m,ans=0,mid;
while(L<=R)
if(f(mid=(L+R)>>1)>=0) ans=mid,L=mid+1;
else R=mid-1;
return ans;
}
signed main(){
freopen("c.in","r",stdin);
freopen("c.out","w",stdout);
scanf("%lld",&m);
for(int i=1;i<=m;i++) scanf("%lld",&w[i]);
for(int i=1;i<=m;i++) t[0].add(i,w[i]),t[1].add(i,i*w[i]);
scanf("%lld",&q);
for(int i=1,x,y;i<=q;i++){
scanf("%lld%lld",&x,&y);
t[0].add(x,y-w[x]),t[1].add(x,x*(y-w[x]));
w[x]=y;
int d=find_d();
int A=t[0].qry(1,m)-t[0].qry(1,d)+t[0].qry(d+1,m);
int B=t[1].qry(d+1,m)-t[1].qry(1,d);
printf("%lld\n",B%mod*inv(A%mod)%mod);
}
return 0;
}
T4
题面
500ms, 64MB.
小 X 最近看了亚瑟王的传说,幻想自己也能成为这样的传奇人物。
在梦中,小 X 看到前方由近及远有 \(n\) 个高度互不相同且均为整数的台阶,其中最矮的高度为 \(1\),最高的高度为 \(n\)。
小 X 隐约听见有人在说话:“拔出此石中剑者,即为英格兰之王。”顺着声音的方向,小 X 看见了一把插在石缝中的装饰华美的剑。
“莫非这就是……”小 X 跑过去,恨不得立刻将其拔出。
然而,小 X 接近它时就会被弹开。刚刚的声音再次响起:“看到那些台阶了吗?要想拔出石中剑,首先要跨过那些台阶。你可以选择从任意一个台阶出发,每次向前跨越若干个台阶,但必须保证每次落脚的台阶都高于上一次落脚的台阶。为了展现王的姿态,你要让落脚的次数尽量多。”
这可难不倒小 X,他轻松地完成了任务,步伐在天空中划出了一道优美的弧线。
“最后,你还要在心中默念一个数,才能得到石中剑的认可。记住自己刚才的轨迹了吗?你看那些台阶,其实都是虚幻的,可以任意改变顺序。石中剑需要你回答的是,那些台阶有多少种不同的排列方法,可以用你刚才的轨迹来完成之前的任务呢?”
思考了许久,小 X 身上直冒汗。身为正义使者的你,想要帮助他在梦中成为英格兰之王。为此,你潜入了他的意识,得到了他刚才的轨迹。现在,你必须尽快得到答案,从而放入他的意识,使他通过石中剑的考验。
\(n \leq 15\),答案小于 \(2^{31}\)。
sol
挺好玩的状压题。
首先要会 \(O(n \log n)\) 的单调栈求最长上升子序列。插入 \(a_i\) 时,找到栈中最小的 \(\ge a_i\) 的元素并将其用 \(a_i\) 替换,如果找不到就插入到栈顶。最后栈内元素个数就是答案。
\(n \leq 15\) 启示我们状压,而 64MB 的空间卡的过 \(3^{15}\) 个int,考虑三进制状压。每次在序列末尾插入一个数,\(0\) 表示还没有插入序列中,\(1\) 表示插入了序列中,而且在序列的单调栈中。\(2\) 表示插入了序列但不在单调栈中。
由题意,一个序列合法,当最长上升子序列长度恰好为 \(k\),且包含给定的长度为 \(k\) 的子序列。所以在插入某个数的时候,如果它在给定的子序列中,则要保证它以前的数全部被插入过。而且单调栈的长度不能超过 \(k\)。每次要插入一个数的时候,模拟插入到单调栈的过程即可。
替换一个数时,将被替换的数代表的数位设为 \(2\),将新插入的数代表的数位设为 \(1\) 即可。\(f_{S'} \gets f_{S'} + f_S\)。
#include<cstdio>
const int M=14348908;
int pw3[16];
int n,k,a[16],f[M],pre[16];
int bit[16],cntbit;
int bit1[16],cnt1;
int ans;
void getbit(int S){
for(int i=0;i<16;i++) bit[i]=bit1[i]=0;
cntbit=cnt1=0;
while(S)
bit[++cntbit]=S-(S/3)*3,S/=3;
}
int main(){
freopen("sword.in","r",stdin);
freopen("sword.out","w",stdout);
pw3[0]=1;
for(int i=1;i<16;i++) pw3[i]=pw3[i-1]*3;
scanf("%d%d",&n,&k);
for(int i=1;i<=k;i++)
scanf("%d",&a[i]),pre[a[i]]=a[i-1];
f[0]=1;
for(int S=0;S<pw3[n];S++){
if(!f[S]) continue;
getbit(S);
int tot=0,mxend=0;
for(int i=1;i<=cntbit;i++){
if(bit[i]) tot++;
if(bit[i]==1) bit1[++cnt1]=mxend=i;
}
if(tot==n) ans+=f[S];
int cur=1;
for(int i=1;i<=n;i++)
if(!bit[i]&&(!pre[i]||bit[pre[i]])){
if(i>mxend){
if(cnt1<k) f[S+pw3[i-1]]+=f[S];
}else{
for(;cur<=cnt1;cur++)
if(i<bit1[cur]&&(i>bit1[cur-1]||cur==1)) break;
if(cur<=cnt1)
f[S+pw3[i-1]+pw3[bit1[cur]-1]]+=f[S];
}
}
}
printf("%d",ans);
return 0;
}
2023.7.21
\(100 + 100 + 100 + 100\)
T1
题面
Bessie 正在安排前往牛尼亚的一次出差,那里有 \(N\)(\(2 \leq N \leq 1000\))个编号为 \(1 \ldots N\) 的城市,由 \(M\)(\(1 \leq M \leq 2000\))条单向的道路连接。Bessie 每次访问城市 \(i\) 都可以赚到 \(m_i\) 哞尼(\(0 \leq m_i \leq 1000\))。从城市 \(1\) 出发,Bessie 想要赚到尽可能多的哞尼,最后回到城市 \(1\)。为了避免争议,\(m_1=0\)。
沿着两个城市之间的道路移动需要消耗一天。出差的准备工作十分费钱;旅行 \(T\) 天需要花费 \(C \times T^2\) 哞尼(\(1 \leq C \leq 1000\))。
Bessie 在一次出差中最多可以赚到多少哞尼?注意有可能最优方案是 Bessie 不访问城市 \(1\) 之外的任何城市,在这种情况下结果应当为 \(0\)。
sol
\(1 \leq m_i \leq 1000\),显然 \(T > 500\) 时一定不优,因为最开始要花 \(c \times T^2\) 哞尼,当 \(T > 500\) 时不论新走到一个城市得到最多的钱也弥补不了旅行自带的损失。
设 \(f_{T, u}\) 为走了 \(T\) 天到达点 \(u\) 能够得到的最多的哞尼。转移 \(f_{T, u} = \max\limits_{(v, u) \in E}(f_{T, u}, f_{T - 1, v} + m_u)\),初始 \(f_{0, 1} = 0\)。最后求出 \(f_{T, x} - c \times T^2\) 的最大值即可。
为了保险 \(T\) 枚举到了 \(1000\)。
#include<cstdio>
#include<cstring>
const int M=2023;
int max(int A,int B){
return A>B?A:B;
}
int head[M],cnte;
struct Edge{int to,next;}e[M];
void add(int u,int v){
e[++cnte]=(Edge){v,head[u]};
head[u]=cnte;
}
int n,m,c,ans,a[M];
int f[M][M];
int main(){
freopen("time.in","r",stdin);
freopen("time.out","w",stdout);
memset(head,-1,sizeof head);
memset(f,-0x3f,sizeof f),f[0][1]=0;
scanf("%d%d%d",&n,&m,&c);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1,u,v;i<=m;i++)
scanf("%d%d",&u,&v),add(u,v);
for(int T=1;T<=1000;T++)
for(int u=1;u<=n;u++)
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
f[T][v]=max(f[T][v],f[T-1][u]+a[v]);
}
ans=-1e9-7;
for(int T=0;T<=1000;T++) ans=max(ans,f[T][1]-c*T*T);
printf("%d",ans);
return 0;
}
T2
题面
[USACO20JAN] Farmer John Solves 3SUM G
Farmer John 相信他在算法设计上实现了一个重大突破:他声称他发现了一个 3SUM 问题的近似线性时间算法,这是一个有名的算法问题,尚未发现比运行速度比平方时间明显更优的解法。3SUM 问题的一个形式是:给定一个整数数组 \(s_1, \ldots, s_m\),计算不同索引组成的无序三元对 \(i, j, k\) 的数量,使得 \(s_i + s_j + s_k = 0\)。
为了测试 Farmer John 的断言,Bessie 提供了一个 \(N\) 个整数组成的数组 \(A\)(\(1 \leq N \leq 5000\))。Bessie 还会进行 \(Q\) 次询问(\(1 \leq Q \leq 10^5\)),每个询问由两个索引 \(1 \leq a_i \leq b_i \leq N\) 组成。对于每个询问,Farmer John 必须在子数组 \(A[a_i \ldots b_i]\) 上求解 3SUM 问题。
不幸的是,Farmer John 刚刚发现了他的算法中的一个错误。他很自信他能修复这个算法,但同时,他请你帮他先通过 Bessie 的测试!
保证对于每个数组元素 \(A_i\) 有 \(-10^6 \leq A_i \leq 10^6\)。
sol
\(N, Q\) 的范围暗示 \(O(N^2)\) 预处理 \(O(1)\) 查询。
\(A_i + A_j + A_k = 0\),令 \(i < j < k\),然后枚举 \(i, k\),求出 \(F_{i, k}\) 为 \([i + 1, k - 1]\) 中有多少个数等于 \((-A_i - A_k)\)。因为值域 \([-10^6, 10^6]\) 所以可以用桶来统计。
对于每个询问 \([l, r]\),实际上要求的就是 \(\sum\limits_{i = l}^r \sum\limits_{k = i + 2}^r F_{i, k}\)。而 \(k < i + 2\) 时 \(F_{i, k} = 0\) 所以只要求 \(F\) 数组 \((l, l) \sim (r, r)\) 的和即可,显然二维前缀和。
#include<cstdio>
const int M=5050,N=2e6+10;
const int delta=2e6;
int n,q,a[M];
long long f[M][M];
int buc[N+delta];
long long qry(int x0,int y0,int x1,int y1){
return f[x1][y1]-f[x0-1][y1]-f[x1][y0-1]+f[x0-1][y0-1];
}
int main(){
freopen("threesum.in","r",stdin);
freopen("threesum.out","w",stdout);
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
f[i][j]=buc[-a[i]-a[j]+delta];
buc[a[j]+delta]++;
}
for(int j=i+1;j<=n;j++) buc[a[j]+delta]=0;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+f[i][j];
for(int i=1,l,r;i<=q;i++){
scanf("%d%d",&l,&r);
printf("%lld\n",qry(l,l,r,r));
}
return 0;
}
T3
题面
Bessie 在一个仅允许沿平行于坐标轴方向移动的二维方阵中。她从点 \((0,0)\) 出发,想要到达 \((N,N)\)(\(1 \leq N \leq 10^9\))。为了帮助她达到目的,在方阵中有 \(P\)(\(1 \leq P \leq 10^5\))个跳板。每个跳板都有其固定的位置 \((x_1,y_1)\),如果 Bessie 使用它,会落到点 \((x_2,y_2)\)。
Bessie 是一个过程导向的奶牛,所以她仅允许她自己向上或向右行走,从不向左或向下。类似地,每个跳板也设置为不向左或向下。Bessie 需要行走的距离至少是多少?
sol
一眼二维偏序 DP。
将所有跳板拆成两个点,这样共有 \(2p\) 个点。按照横坐标排序,将纵坐标离散化。设 \(f_i\) 为走到第 \(i\) 个点时最小路程,则 \(f_i = \min\limits_{x_j < x_i, y_j < y_i}(f_j + (x_i - x_j) + (y_i - y_j))\),即 \(f_i - x_i - y_i = \min\limits_{x_j < x_i, y_j < y_i}(f_j - x_j - y_j)\),故维护 \(g_i = f_i - x_i - y_i\) 的最小值即可。第一维通过排序自然满足,第二维查询前缀最小值,上树状数组即可。
如果 \((x_i, y_i)\) 是某个跳板的出发点,那么它对到达点也有贡献,\(g_{i'} \gets g_i + x_i + y_i - x_{i'} - y_{i'}\)。
最后要走到 \((N, N)\),所以答案是 \(\min f_i + 2N\)。
#include<cstdio>
#include<algorithm>
const int M=2e5+10;
int min(int A,int B){
return A<B?A:B;
}
int n,m,f[M],nxt[M];
struct node{int x,y,y0,id;}p[M];
int lsh[M],len;
bool cmp(node A,node B){
return A.x<B.x||A.x==B.x&&A.y<B.y;
}
int c[M];
const int INF=1e9+7;
int lowbit(int x){return x&-x;}
void ins(int x,int y){
for(;x<=len;x+=lowbit(x)) c[x]=min(c[x],y);
}
int qry(int x){
int ret=INF;
for(;x;x-=lowbit(x)) ret=min(ret,c[x]);
return ret;
}
int main(){
freopen("boards.in","r",stdin);
freopen("boards.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&p[i].x,&p[i].y,&p[i+m].x,&p[i+m].y);
lsh[++len]=p[i].y,lsh[++len]=p[i+m].y;
p[i].id=i,p[i+m].id=i+m;
}
std::sort(lsh+1,lsh+1+len);
len=std::unique(lsh+1,lsh+1+len)-lsh-1;
for(int i=1;i<=2*m;i++)
p[i].y0=std::lower_bound(lsh+1,lsh+1+len,p[i].y)-lsh;
std::sort(p+1,p+2*m+1,cmp);
for(int i=1;i<=2*m;i++)
if(p[i].id>m) nxt[p[i].id-m]=i;
int mn=INF;
for(int i=1;i<=2*m;i++){
f[i]=min(f[i],qry(p[i].y0)),ins(p[i].y0,f[i]);
if(p[i].id<=m){
int tmp=nxt[p[i].id];
f[tmp]=min(f[tmp],f[i]+p[i].x+p[i].y-p[tmp].x-p[tmp].y);
}
mn=min(mn,f[i]);
}
printf("%d",mn+2*n);
return 0;
}
T4
题面
Bessie 成为了一名艺术家,正在创作壁画!她现在正在创作的作品是一个高为 \(N\) 的方阵,方阵的每行都由 \(M\) 个方格组成(\(1\le N, M \le 1000\))。每个方格是空的,画了石头,或者画了水。Bessie 已经画上了包含石头的方格,包括整幅画作的边界。她现在想要将某些空的方格画上水,使得如果这幅画是真实的,其中应当不存在水的净移动。定义从上到下第 \(i\) 行的方格的高度为 \(N+1-i\)。Bessie 想要她的画作满足以下限制:
假设方格 \(a\) 画的是水。那么如果存在一条从 \(a\) 到方格 \(b\) 的路径,由高度不超过 \(a\) 的空的方格或是有水的方格组成,路径中每相邻两个方格都有一条公共边,那么 \(b\) 画的也是水。
求 Bessie 可以创作的不同作品的数量模 \(10^9+7\) 的余数。Bessie 可以将任意数量的空格画上水,包括不画以及全画。
sol
根据乘法原理,答案为各联通块答案之积。
可以用并查集来维护联通块。考虑从下向上扫,如果有两个联通块合并,那么新的联通块答案应该是两个联通块的方案之积。如果是通过 \(h+1\) 层连通了两个第 \(h\) 层的联通块,那么新的联通块的答案是原来两个联通块的答案之积加一(在第 \(h+1\) 层放水)。这些在并查集上维护即可。
#include<cstdio>
#include<cstring>
const int M=1010,N=1e6+10;
const int mod=1e9+7;
int n,m; char mp[M][M];
int get(int x,int y){
return (x-1)*m+y;
}
int fa[N],f[N];
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y){
x=find(x),y=find(y);
if(x!=y) fa[y]=x,f[x]=1ll*f[x]*f[y]%mod;
}
const int dx[]={-1,0,0},dy[]={0,-1,1};
bool vis[N];
int main(){
freopen("cave.in","r",stdin);
freopen("cave.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n*m;i++) fa[i]=i,f[i]=1;
for(int i=1;i<=n;i++) scanf(" %s",mp[i]+1);
for(int i=n-1;i>=2;i--){
for(int j=2;j<m;j++)
if(mp[i][j]=='.')
for(int dir=0;dir<3;dir++)
if(mp[i+dx[dir]][j+dy[dir]]=='.')
merge(get(i+dx[dir],j+dy[dir]),get(i,j));
for(int j=2;j<m;j++)
if(mp[i][j]=='.'){
int tmp=find(get(i,j));
if(!vis[tmp])
vis[tmp]=true,(++f[tmp])%=mod;
}
for(int j=2;j<m;j++)
if(mp[i][j]=='.') vis[find(get(i,j))]=false;
}
int ans=1;
for(int i=1;i<=n*m;i++)
if(find(i)==i) ans=1ll*ans*f[i]%mod;
printf("%d",ans);
return 0;
}

浙公网安备 33010602011771号