考试总结 2026.2.26

闲话

既然是闲话,那就等闲了再写。

时间分配

题目 思考时间 打码时间 调试时间 重写次数 得分
T1 情报网络 10 min 15 min 5 min 1 100
T2 互不攻击的马 0 min 15 min 0 min 0 100
T3 好区间 30 min 40 min 30 min 1 100

题目总结

难度还行。

T1 有点绕,但是理清了代码就很简单。

T2 几乎就是原题,谴责样例出锅了。

T3 先把 \(n=k\) 的部分分拿了,然后推 \(k=1\) 的时候想出了正解。

题目

T1 情报网络 network

题目
一个特工的一生:

  1. 在第 \(i\) 天接收到情报后,第 \(i\sim i+D-1\) 天处在蛰伏期,不传递情报。
  2. 从第 \(i+D\) 天开始,每天给一个不知道情报的特工传递情报。
  3. 在第 \(i+F\) 天时,忘掉情报,停止传递。

在第一天,一个特工收到了情报。

询问在 \(N\) 天后有多少个特工了解了情报。

赛时思路
首先注意到对于一个特工,有三个时间点很重要:收到情报的时间(蛰伏期开始时间),传递开始时间,遗忘时间。

中间的时间太多了,也没有区别。

于是考虑定义三个数组。

\(dp_{1_i}\):第 \(i\) 天新增蛰伏期的人数。
\(dp_{2_i}\):第 \(i\) 天新增传递期的人数。
\(dp_{3_i}\):第 \(i\) 天遗忘的人数。

但是我们发现,要计算新增的蛰伏期人数,需要知道所有传递期的人数,于是我们考虑用一个变量 \(ans\) 表示当前传递期的总人数。

于是有:

dp_1[1]=1;
for(int i=D+1;i<=n;i++){
	dp_3=dp_1[i-F];
	dp_2[i]=dp_1[i-D];
	ans+=dp_2[i];
	ans-=dp_3[i];
	dp_1[i]=dp_2;
}

然后我们发现,\(dp_2\)\(dp_3\) 这两个数组根本没用,于是可以省略掉。

dp_1[1]=1;
for(int i=D+1;i<=n;i++){
	ans+=dp[i-D]-(i-F<0?0:dp[i-F]);
	dp[i]=ans;
}

循环结束后,\(ans\) 并不是最终的答案,因为知道情报的特工还包括蛰伏期的。

code

#include<bits/stdc++.h>
#define int long long
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
const int N=1e6+5;
const int mod=1e9+7;
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*10+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
int n,m,k;
int T;
int D,F;
int dp[N];//新增蛰伏期
int ans;//总共传递
signed main(){
	n=read(),D=read(),F=read();
	dp[1]=1;
	for(int i=D+1;i<=n;i++){
		ans=(ans+dp[i-D]-(i-F<0?0:dp[i-F]))%mod;
		dp[i]=ans;
	}
	for(int i=n-D+1;i<=n;i++)ans=(ans+dp[i])%mod;
	print((ans%mod+mod)%mod);
}

T2 互不攻击的马 horse

题目
在一个 \(X\)\(Y\) 列的棋盘上放国际象棋的马[1],有多少种方法。

思路
题目很简单,和之间做过的互不侵犯很类似,只不过这道题要记录前两行的状态。

code

#include<bits/stdc++.h>
#define int long long
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
const int mod=1e9+7;
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*10+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
int n,m,k;
int T;
int dp[105][1<<6][1<<6];
signed main(){
//	freopen("horse.in","r",stdin);
//	freopen("horse.out","w",stdout);
	n=read(),m=read();
	for(int s=0;s<(1<<m);s++)dp[1][s][0]=1;
	for(int i=2;i<=n;i++){
		for(int s=0;s<(1<<m);s++){
			for(int t=0;t<(1<<m);t++){
				if((s&(t>>2))||(s&(t<<2)))continue;
				for(int l=0;l<(1<<m);l++){
					if((t&(l<<2))||(t&(l>>2)))continue;
					if((s&(l<<1))||(s&(l>>1)))continue;
					dp[i][s][t]=(dp[i][s][t]+dp[i-1][t][l])%mod;
				}
			}
		}
	}
	int ans=0;
	for(int s=0;s<(1<<m);s++){
		for(int t=0;t<(1<<m);t++){
			if((s&(t<<2))||(s&(t>>2)))continue;
			ans=(ans+dp[n][s][t])%mod;
		}
	}
	print(ans);
}

那如果加入变式:中国象棋的马[2],有撇脚。

这时候就不能简单地判断是否能被吃了。

于是……

暴 力 大 法!

bool has(int s,int x){
	return s&(1<<x);
}
void init(){
	for(int s=0;s<(1<<m);s++){
		for(int t=0;t<(1<<m);t++){
			for(int l=0;l<(1<<m);l++){
				ok[s][t][l]=1;
				for(int i=0;i<m;i++){
					if(i>0)if(has(s,i)&&has(l,i-1)&&!(has(t,i)&&has(t,i-1)))ok[s][t][l]=0;
					if(i<m-1)if(has(s,i)&&has(l,i+1)&&!(has(t,i)&&has(t,i+1)))ok[s][t][l]=0;
					if(i>1)if(has(s,i)&&has(t,i-2)&&!(has(s,i-1)&&has(t,i-1)))ok[s][t][l]=0;
					if(i<m-2)if(has(s,i)&&has(t,i+2)&&!(has(s,i+1)&&has(t,i+1)))ok[s][t][l]=0;
					if(i>1)if(has(t,i)&&has(l,i-2)&&!(has(t,i-1)&&has(l,i-1)))ok[s][t][l]=0;
					if(i<m-2)if(has(t,i)&&has(l,i+2)&&!(has(t,i+1)&&has(l,i+1)))ok[s][t][l]=0;
				}
			}
		}
	}
}

那如果再将 \(m\) 调大一点呢?比如 8。

这个时候就要用到轮廓线了。

简单来说,对于本题,能够影响 \((i,j)\) 放马的只有几个点,为了方便压缩状态,我们将下图中绿色的点全都记录状态:
捕获

于是我们 DP 数组的状态就只需要 \(dp_{i,j,s}\):选到 \((i,j)\) 的时候前面绿色点状态为 \(s\) 的方案数。

那么绿色点的边界到底是什么呢?

观察上图,编号为 1 的点其实就是黄色的点向上可以吃到的最左边的点。

code

#include<bits/stdc++.h>
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int mod=1e9+7;
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-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int dp[105][9][1<<17];
int get_id(int x,int y){
	return (x-1)*m+y;
}
int has(int s,int now_x,int now_y,int x,int y){
	if(x<1||y<1||x>n||y>m)return 0;
	int now_id=get_id(now_x,now_y);
	int id=get_id(x,y);
	int delta=abs(now_id-id)-1;
	return s&(1<<delta);
}
int len;
signed main(){
	//ios::sync_with_stdio(0);
	n=read(),m=read();
	dp[1][1][1]=1;
	dp[1][1][0]=1;
	len=m*2+1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			for(int s=0;s<(1<<len);s++){
				if(j!=m){
					int new_s=(s<<1)&((1<<len)-1);
					dp[i][j+1][new_s]+=dp[i][j][s];
					dp[i][j+1][new_s]%=mod;
					new_s=((s<<1)|1)&((1<<len)-1);
					int I=i,J=j+1;
					if((!has(s,I,J,I-1,J-2))&&(!has(s,I,J,I-2,J-1))){
						if((!has(s,I,J,I-2,J+1))&&(!has(s,I,J,I-1,J+2))){
							dp[i][j+1][new_s]+=dp[i][j][s];
							dp[i][j+1][new_s]%=mod;
						}
					}
				}
				else{
					int new_s=(s<<1)&((1<<len)-1);
					dp[i+1][1][new_s]+=dp[i][j][s];
					dp[i+1][1][new_s]%=mod;
					new_s=((s<<1)|1)&((1<<len)-1);
					int I=i+1,J=1;
					if((!has(s,I,J,I-1,J-2))&&(!has(s,I,J,I-2,J-1))){
						if((!has(s,I,J,I-2,J+1))&&(!has(s,I,J,I-1,J+2))){
							dp[i+1][1][new_s]+=dp[i][j][s];
							dp[i+1][1][new_s]%=mod;
						}
					}
				}
			}
		}
	}
	int ans=0;
	for(int s=0;s<(1<<len);s++){
		ans=(ans+dp[n][m][s])%mod;
	}
	print(ans);
}

T3 好区间 good

题目
求一个序列满足要求的子区间数量。
要求:区间的数值集合不能出现长度超过 \(k\) 的连续段。
举个例子:当 \(k=3\) 时,集合 \(\{1,2,2,3\}\) 合法,而区间 \(\{1,2,2,3,4\}\) 不合法。

思路
首先看到了特殊性质:\(k=n\),这个时候所有子区间都是合法的,答案就是:

\[\sum_{i=1}^{n}i \]

然后我继续考虑特殊性质:\(k=1\)

这时候,要求就是若 \(x\) 出现,则 \(x-1\)\(x+1\) 都不会出现。这个的实现很简单,用 map 标记一下即可。

然后考虑在 \(O(N)\) 时间内求合法区间。

首先明确:当一个区间合法时,它的子区间全部合法。

于是我想到了用双指针的方法来维护:

对于每个 \(i\in[1,n]\),我们可以找到最大的 \(r\) 使区间 \([i,r]\) 合法。并且随着 \(i\) 的增加,\(r\) 只会递增而不会下降。

此时 \(i\) 会遍历 \(1\sim n\)\(r\) 也只会遍历一遍 \(1\sim n\),时间允许。

然后考虑计算答案,我当时想的是:既然区间 \([i,r]\) 合法,那么一共 \(\frac{(r-i+1)(r-i+2)}{2}\) 个子区间全部合法,对答案的贡献就是其子区间数量。

但是我发现这样会重复,对于 \(i\)\(i+1\),区间 \([i+1,r]\) 可能会被计算两遍。

通过上面的错误原因,也可以很显然地得出改正方法:每次固定区间左端点,所以合法区间 \([i,r]\) 的贡献就是 \(r-i+1\)

然后正解就已经出来了。

对于 \(k=1\) 可以用 map 维护,那么对于任意的 \(k\),拿什么维护?

很明显,我们需要维护一个动态区间内的最大连续段长度。这是什么?这就是线段树区间合并的板子!

code

/*
  
  
  Oh no!
  肯定是双指针。
  
  
 */
#include<bits/stdc++.h>
#define lc f[p].l
#define rc f[p].r
#define int long long
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
const int N=2e5+5;
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*10+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
int n,m,k;
int T;
int a[N];
struct node{
	int l,r,lmx,rmx,mx,ans,cnt;
}f[5*N];
int rt;
int idx;
void pushup(int p,int L,int R){
	if(!lc)lc=++idx;
	if(!rc)rc=++idx;
	f[p].cnt=f[lc].cnt+f[rc].cnt;
	f[p].lmx=f[lc].lmx;
	int mid=L+R>>1;
	if(f[lc].lmx==mid-L+1)f[p].lmx=mid-L+1+f[rc].lmx;
	f[p].rmx=f[rc].rmx;
	if(f[rc].rmx==R-mid)f[p].rmx=R-mid+f[lc].rmx;
	f[p].mx=max({f[lc].mx,f[rc].mx,f[lc].rmx+f[rc].lmx});
}
void update(int &p,int L,int R,int x,int k){
	if(!p){
		p=++idx;
		f[p]=node{0,0,0,0,0,0,0};
	}
	if(x<L||x>R)return;
	if(L==R){
		f[p].ans+=k;
		if(f[p].ans){
			f[p].lmx=f[p].rmx=f[p].mx=f[p].cnt=1;
		}
		else{
			f[p].lmx=f[p].rmx=f[p].mx=f[p].cnt=0;
		}
		return;
	}
	int mid=L+R>>1;
	update(lc,L,mid,x,k);
	update(rc,mid+1,R,x,k);
	pushup(p,L,R);
}
signed main(){
	//	freopen("good.in","r",stdin);
	//	freopen("good.out","w",stdout);
	n=read(),k=read();
	int mx=0;
	for(int i=1;i<=n;i++)a[i]=read(),mx=max(mx,a[i]);
	if(n==k){
		int ans=0;
		for(int i=1;i<=n;i++)ans+=i;
		print(ans);
		return 0;
	}
	int ans=0;
	update(rt,1,200000,a[1],1);
	int r=1;
	while(r+1<=n){
		update(rt,1,200000,a[r+1],1);
		if(f[rt].mx>k){
			update(rt,1,200000,a[r+1],-1);
			break;
		}
		r++;
	}
	ans+=r;
	for(int i=2;i<=n;i++){
		update(rt,1,200000,a[i-1],-1);
		while(r+1<=n){
			update(rt,1,200000,a[r+1],1);
			if(f[rt].mx>k){
				update(rt,1,200000,a[r+1],-1);
				break;
			}
			r++;
		}
		ans+=r-i+1;
	}
	print(ans);
}

最后

不难,跟刘平大赛一样

我 AK 了,哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈

我要吃虾片!


  1. 国际象棋的马没有撇脚,如下图:
    马的攻击范围 ↩︎

  2. 洛谷-P5005 ↩︎

posted on 2026-02-26 20:10  fish2012  阅读(3)  评论(0)    收藏  举报