集美大学第十三届蓝桥校选题解

目录

  • 赛事总结
    • PTA分享码
    • 前言
    • 比赛结果
    • 出题组
    • 题面
      • 大体格式
      • 题面严谨
      • 数据设置
      • 出题手册
      • 陈述风格
    • 题目风格
    • 题目难度
    • 题目质量、数据
  • 签到题
    • 签到题1-装13
      • 题目大意
      • 出题报告
        • 测试点详情
      • 解题思路
        • 算法一:计数
          • 算法思路
          • 代码实现
        • 算法二:位段
          • 算法思路
          • 代码实现
    • 签到题2-暗箱操作的勋总
      • 题目大意
      • 出题报告
      • 解题思路
      • 代码实现
  • 中档题
    • 中档题1-消息通知
      • 题目大意
      • 出题报告
      • 解题思路
      • 代码实现
    • 中档题2-勋总的幂集
      • 题目大意
      • 出题报告
      • 解题思路
      • 代码实现
    • 中档题3-CrazyRabbits
      • 题目大意
      • 出题报告
        • 前言
        • 测试点详情
      • 解题
        • 算法1:快速幂
          • 算法思路
          • 代码实现
          • 处理方式①:逆元
            • 算法思路
            • 代码实现
          • 处理方式②:不采用逆元
            • 算法思路
            • 代码实现
        • 算法2:矩阵快速幂
          • 算法思路
          • 代码实现
    • 中档题4-勋总的完美基因
      • 题目大意
      • 出题报告
        • 前言
        • 测试点数据
      • 解题思路
      • 代码实现
    • 中档题5-LIS
      • 题目大意
      • 出题报告
        • 前言
        • 测试点详情
      • 解题
        • 算法
          • 算法思路1.0
          • 代码实现1.0
          • 算法思路2.0
          • 代码实现2.0
    • 中档题6-勋总的求偶日记
      • 题目大意
      • 出题报告
        • 测试点详情
      • 解题思路
        • 状态初始化
        • 反向枚举
        • 如何枚举
        • 验题问题
      • 代码实现
    • 中档题7-何老师的课
      • 题目大意
      • 出题报告
      • 解题思路
      • 代码实现
    • 中档题8-回文串判断
      • 题目大意
      • 出题报告
      • 解题思路
      • 代码实现
  • 压轴题
    • 压轴题1-贝贝的数组划分
      • 题目大意
      • 出题报告
        • 测试点详情
      • 解题思路
      • 代码实现
    • 压轴题2-别装 13
      • 题目大意
      • 出题报告
      • 解题思路
      • 代码实现

赛事总结

PTA分享码

  • 本场比赛的题目集PTA教师账号分享码为:94EB5349286B5A86
  • 题目集开源,需要的友校ACM选手自取

前言

  • 这套卷子的目的就是训练一种蓝桥做题方法,前半场简单题AC,后面所有的难题都暴力骗分,当然骗分并非单纯的骗分,而是基于一定策略的,搜索骗分,二进制枚举等等。
  • 换句话说,这套卷子本身就是不打算让大部分同学一路AC过去的
  • 本场校选最优策略是:而是前半场A题,中后半场骗分,或者集中时间尝试AC一两道对于自身水平的难题。

比赛结果

  • 本场比赛的签到率为 86 % 86\% 86%,同我们预期的 80 % ∼ 90 % 80\%\sim 90\% 80%90%,圆了想拿创新学分同学的梦
  • 整体榜单非常惨,不过也是预料之中(要的就是这个效果)
  • 榜一勋总 140 140 140分,赛后勋总说是懒得骗分,预估勋总还可以再骗 6 + 10 = 16 6+10=16 6+10=16分,但是那时候他觉得没啥意义,想回去A出某题。然后理院天花板何大佬直接演起来,开局防 A K AK AK题起手,进行两小时半的挣扎终于A出来了。(集体演员?)
  • 出题组的预期是榜一在 [ 140 , 170 ] [140,170] [140,170]之间(满分 250 250 250),but难度相当大的,因为从中档-3开始就很有区分度了,这个分数还是很不容易的。

出题组

由BCD出题组出题(因为最强的 A A A(勋总)去参赛了,所以由 A B C ABC ABC出题组变名为 B C D BCD BCD出题组)

组长:贝
组员:杰、弛、举、廖、曾

题面

大体格式

  • 细心的同学会发现,本次所有题目的题面都按照以下格式给出

题目陈述
输入格式
输出格式
输入样例
输出样例
(可选)样例解释
数据规模及约定

  • 并不同天梯校选,因为蓝桥省赛大致按这一种形式给出。
  • 对于较晦涩的题意,我们会给出样例解释,一定意义降低了题目难度

题面严谨

  • 并且每题的数据范围都给的,和题面陈述逻辑都给的很详细(我监督了整个出题组的成员的题面),多数题面修改了不下三四次(可以想象原本的题面是真的做题体验不好
  • (虽然有人反应了一点:压轴-2有人确实有点不符合常理,硬凑题意,比较容易让人自我怀疑,是否读错了题意)

数据设置

  • 这次设计的数据,因为蓝桥比赛是 O I OI OI赛制,我们的初衷是尽量保证黑箱测试

  • 因为PTA上的赛制无法设置OI赛制,故我们只能设置平时的IOI赛制

  • 除了签到题,可以通过 I O I IOI IOI赛制二分找出测试点的具体值以外,或者直接YESNO就有5分,我们是有意为之,并不打算设置多组数据来卡大家签到(但是在真正的比赛中,这类YESorNO的题目,必然是多组数据的,防止有人直接输出YES可以得到一半分数)

  • 而对于其他题目,比如勋总的求偶日记,若不设置多组数据,则可以二分数据点,然后本地暴力跑,得出答案,再打表提交上去AC,这在OI赛制中是不可能出现的,而且多组数据也减小了随机数无技术含量,以及错误算法能骗分的可能性。

    (这也是往年我校出题组出的卷子数据存在的问题,有人校赛可以靠这样拿很高分数,正式赛就翻车的原因)

出题手册

  • 虽然是六个人出的题目,但是我尽力统一了陈述方式(包括题解),为此我不仅写了出题手册,还让所有组员写了出题报告
    集美大学第十三届蓝桥校选题解_第1张图片
  • 并且对题解也做了约束
    集美大学第十三届蓝桥校选题解_第2张图片

陈述风格

  • 这场比赛我在输入输出格式上面做了统一,大致如下
  • 后来又放宽,可以去掉包含,然后就呈现了给大家的题目
    集美大学第十三届蓝桥校选题解_第3张图片

题目风格

有以下题目非蓝桥题风原因如下:
签到题1、签到题2,非蓝桥题风,为了考虑想拿创新学分的同学,同时一定意义综合了按位枚举的蓝桥杯暴力法
中档题1:非蓝桥题分,并查集模板,集体送分,为了榜单不那么难看,大家的做题体验不那么差hhh
中档题2:非蓝桥题风,枚举子集,是蓝桥省赛中常用的骗分手段
中档3-压轴2:蓝桥题风,整体后半部分卷子结构“动态规划+数据结构+字符串”,然后数论嵌套在各个题目当中。

题目难度

  • 大致同蓝桥省赛难度
  • 可能略微高一些,但是给出考纲,采用IOI赛制都是降低比赛难度的一种策略

题目质量、数据

  • 整场卷子我们演了三次卷子,所以正常比赛也算顺利(中途临时加时限,就是为了送分hhh)

  • U1S1说题目质量确实很高,给很多ACM银牌选手演过了题目,质量还算高,但是还是有一定的难度

  • (题目质量和idea确实还不错
    集美大学第十三届蓝桥校选题解_第4张图片

  • 因为现应对蓝桥省赛时,难度也大致同这次校选

签到题

签到题1-装13

出题人:贝

题目大意

  • 。一个非负整数 n n n的二进制形式里面是否存在连续的 13 13 13 1 1 1

出题报告

测试点详情

  • 测试点1,等价样例
  • 测试点2,17个0,13个1,下列均表示为str(17, '0') + str(13, '1');
  • 测试点3,str(1, '0') + str(13, '1')+ str(4,'0')+str(12,'1');,卡掉仅从低位连续不到13个1就跳出的错误算法。
  • 测试点4,str(2, '0') + str(5, '1') + str(3, '0') + str(20, '1');,测试连续情况大于13个1的情况。
  • 测试点5,str(30, '1'),极端情况,全1
  • 测试点6,str(12, '1') + str(1, '0') + str(12, '1') + str(1, '0') + str(4, '1');,测试NO基本情况,且1的总数大于13但是不连续
  • 测试点7,str(30, '0');,极端情况全0
  • 测试点8,str(6, '0') + str(9, '1') + str(6, '0') + str(9, '1')
  • 测试点9,01循环节15次
  • 测试点10,10循环节15次

解题思路

算法一:计数

算法思路
  • 遇到1cnt++,遇到0cnt清零即可
  • 如果当cnt==13直接输出YES
  • 遍历完毕所有位都没有出现,则直接输出NO
代码实现
#include
int main()
{
    int n;
    scanf("%d", &n);
    int cnt = 0;
    for (int i = 0; i < 30; i ++ )
    {
        if((n & (1 << i)) == 0)
            cnt = 0;
        else {
            cnt ++ ;
            if(cnt == 13)
            {
                printf("YES\n");
                return 0;
            }
        }
    }
    printf("NO\n");
}

算法二:位段

算法思路
  • 首先构造一段 13 13 13个连续的 1 1 1,十进制形式即, s t = 2 13 − 1 st=2^{13}-1 st=2131
  • 然后取出 n n n的连续 13 13 13位与 s t st st按位与,如果按位与&的结果,依旧为 s t st st,则表示存在连续的 13 13 13 1 1 1
  • 取出 n n n的第 [ i , i + 12 ] [i,i+12] [i,i+12]位,为 n > > i n>>i n>>i,边界条件 i + 13 − 1 < 30 i+13-1<30 i+131<30,即边界小于 30 30 30
  • 如果所有情况都没有,则为NO
代码实现
#include
int main()
{
    int n, st = (1 << 13) - 1; //2的13次方 - 1
    //二进制连续13个1
    scanf("%d", &n);
    for (int i = 0; i + 13 - 1 < 30; i ++ )
    {
        if(((n >> i) & st) == st) //注意运算符优先级
        {
            printf("YES\n");
            return 0;
        }
    }
    printf("NO\n");
}

签到题2-暗箱操作的勋总

出题人:廖

题目大意

给定两个整数x和y,有两个操作,操作1可以花费a元将x或y增加或减少1,操作2可以花费b元将x和y同时增加或减少1,
计算使x=y=0所需花费的最低金额。

出题报告

  • 测试点详情

    第一个测试点:xb,结果在int范围内。
    第二个测试点:x>y,a>b,结果在int范围内。
    第三个测试点:x>y,a=b,结果在longlong范围内。
    第四个测试点:x 第五个测试点:xb,结果在int范围内。

解题思路

  • 很容易想到所有情况,第一种情况是将x和y分别减到0,即(x+y)*a,第二种情况是将x与y同时减少,减到x与y较小的那个数为0为止,
    再将另一个数减到0,即x

代码实现

  • C语言代码
#include
long long x, y, a, b;
int main(){
    scanf("%lld%lld%lld%lld", &x, &y, &a, &b);
    if(x > y) //x中保存小的数
    {
        long long temp = x;
        x = y, y =temp;
    }
    if(2 * a > b) printf("%lld\n", b * x + (y - x) * a);
    else printf("%lld\n", a * (x + y));
}
  • C++代码
#include
using namespace std;
int main()
{
	long long x,y,a,b;
	cin>>x>>y>>a>>b;
	if(x>y) swap(x,y);
	cout<<min(x*b+(y-x)*a,(x+y)*a);
	return 0;
}

中档题

中档题1-消息通知

出题人:举

题目大意

n个同学会形成若干个朋友圈,相当于是若干个集合。把消息通知给某个人需要花费一定代价,但是同一个朋友圈内消息传递不需要代价的,要求把消息通知给所有人的最小花费。

出题报告

  • 前言:这题就是并查集的模板题,只要学过的就会做。
  • 测试点详情
    • 测试点1-5,n为1000以内的随机数,m为30以内的随机数。
    • 测试点6-10 n为30000以内的随机数,m为10000以内的随机数。
    • 每个人所属俱乐部,先随机生成一个数k表示这个人加了多少俱乐部,然后再随机生成k个数表示加了哪几个社团。

解题思路

并查集模板题,只要求出每个同学所属的集合,然后遍历集合中每个人,选则消息通知代价的最小的那个人即可。

代码实现

#include
using namespace std;
const int maxn = 3e4 + 10;

int fa[maxn];
void init() {
	for (int i = 0; i < maxn; i++) {
		fa[i] = i;
	}
}
int find(int cur) {
	return fa[cur]= fa[cur] == cur ? cur : find(fa[cur]);
}


void solve() {
	init();
	int n, m;
	cin >> n >> m;
	vector<int> c(n + 1);
	int i, j;
	for (i = 1; i <= n; i++)cin >> c[i];
	for (i = 0; i < m; i++) {
		int k;
		cin >> k;
		int first;
		cin >> first;
		int num;
		for (j = 1; j < k; j++) {
			cin >> num;
			int r1 = find(first);
			int r2 = find(num);
			fa[r1] = r2;
		}
	}

	unordered_map<int, int> mmid;
	int ans = 0;
	for (i = 1; i <= n; i++) {
		int r = find(i);
		if (mmid.count(r) != 0) {
			mmid[r] = min(mmid[r], c[i]);
		}
		else {
			mmid[r] = c[i];
		}
	}
	for (auto val : mmid) {
		ans += val.second;
	}
	cout << ans << endl;
}

int main() {
	ios::sync_with_stdio(false);
	solve();
	return 0;
}

中档题2-勋总的幂集

出题人:廖


题目大意

给定一个数组,列举出该数组的所有子集(不包含空集),子集按字典序从小到大排序。

出题报告

  • 前言:这道题题意比较简单,思路也很简单,用二进制枚举或dfs都可以求解,但排序可能会卡到一部分人,如果将数字转化成字符串
    处理相对比较麻烦,可以开一个结构体,结构体内放置一个vector,再用结构体排序即可。

  • 测试点详情

    第一个测试点:n=1,仅有一个元素。
    第二个测试点:n=2,两个元素。
    第三个测试点:n=3,三个元素。
    第四个测试点:n=4,四个元素。
    第五个测试点:n=5,五个元素。
    第六个测试点:n=6,六个元素,且元素均小于10。
    第七个测试点:n=7,七个元素,且元素均小于10。
    第八个测试点:n=8,八个元素,且元素均小于10。
    第九个测试点:n=9,九个元素,且元素均小于100。

解题思路

  • 二进制枚举所有状态,1表示该数字被选中,0表示该数字未被选中
  • 将子集用一个vector表示,接着放入set中默认排序
  • 或者,将vector存入到结构体中,排序即可,
    时间复杂度为O(2^N).

代码实现

  • setvector
#include 
using namespace std;
int n;
int totBit;
//{1,2,--,n}
int a[100];
int main()
{
    cin >> n;
	for (int i = 0; i < n ; i ++ )
		cin >> a[i];
	totBit = (1 << n) - 1;
    set<vector<int> > s;
    for (int i = 1; i <= totBit; i ++ )
    {
        // 当前状态为i
        vector<int> v;
        for (int j = 0; j < n; j ++ )
         //总共有n位,为0 ~ (n - 1)
         // 0~ n -1 ---> 1 ~ n 
        {
            // if (i & (1 << j))
            if (((i >> j) & 1) == 1)
            {
                v.push_back(a[j]);
            }
        }
        s.insert(v);
    }

    // O(n log n)
    for (vector<int> v : s)
    {
        for (int i = 0; i < v.size(); i ++ )
        {
			if ( i > 0) cout << ' ';
            cout << v[i];
			
        }
        cout << '\n';
    }
}
  • 排序
#include
using namespace std;
typedef long long ll;
const int maxn=2e6+10;
#define pb push_back
#define rep(i,a,b) for(int i=a;i<=b;i++)
typedef struct{
	vector<int> v;
}node;
node a[2005];
int b[15]; 
bool cmp(node x,node y){
	return x.v<y.v;
}

int main()
{
	int n,cnt=0;
	cin>>n;
	rep(i,1,n) cin>>b[i];
	for(int i=1;i<(1<<n);i++){
		for(int j=1;j<=n;j++){
			if((1<<(j-1))&i) a[cnt].v.pb(b[j]);
		}
		cnt++;
	}
	sort(a,a+cnt,cmp);
	for(int i=0;i<cnt;i++){
		for(int j=0;j<a[i].v.size()-1;j++) cout<<a[i].v[j]<<" ";
		cout<<a[i].v[a[i].v.size()-1];
		cout<<endl;
	}
	return 0;
}

中档题3-CrazyRabbits

出题人:曾

题目大意

a 0 = 1 , a n = 4 × a n − 1 + 2 a_0=1,a_n=4 \times a_{n-1}+2 a0=1,an=4×an1+2 , 求 a n a_n an

出题报告

前言

算法一涉及了高中数学里“构造等比数列”,有做数学题那味了,然后出了这题。

测试点详情

共十个测试点:

  • n = r a n d ( ) % 100 + 100 n=rand()\%100+100 n=rand()%100+100, 多个数据防止骗分。
  • 前三个测试点: 1 < = x < = 3 × 1 0 4 1<=x<=3 \times 10^4 1<=x<=3×104
    循环 x x x次直接能过,占 6 6 6
  • 后七个测试点: x = r a n d ( ) + ( 1 L L < < ( r a n d ( ) % 50 + 1 ) ) ; x = rand() + (1LL << (rand()\%50+1)); x=rand()+(1LL<<(rand()%50+1));
    得用快速幂或者矩阵快速幂,占 14 14 14

解题

算法1:快速幂

算法思路

构造等比数列,使用快速幂
a 0 = 1 , a n = 4 × a n − 1 + 2 a_0=1,a_n=4 \times a_{n-1}+2 a0=1,an=4×an1+2
→ a n + 2 3 = 4 ( a n − 1 + 2 3 ) \rightarrow a_n+\cfrac{2}{3}=4(a_{n-1}+\cfrac{2}{3}) an+32=4(an1+32)
→ a n + 2 3 = 5 3 × 4 n \rightarrow a_n+\cfrac{2}{3}=\cfrac{5}{3} \times 4^n an+32=35×4n
→ a n = 5 3 × 4 n − 2 3 = 5 × 4 n − 2 3 \rightarrow a_n=\cfrac{5}{3} \times 4^n-\cfrac{2}{3}=\cfrac{5 \times 4^n-2}{3} an=35×4n32=35×4n2

快速幂 q p o w ( x , n , k ) : x n % k qpow(x,n,k): x^n\%k qpow(x,n,k):xn%k

代码实现
#define ll long long
const ll mod= 1000000007LL;
ll qpow(ll x,ll n,ll k){
	ll ans=1;
	while(n){
		if(n%2==1)ans=ans*x%k;
		x=x*x%k;
		n/=2;
	}
	return ans;
} 

有分母,需要处理分母:
注意 a b % m o d ≠ a % m o d b \cfrac{a}{b}\%mod \neq {\cfrac{a\%mod}{b}} ba%mod=ba%mod
解释: a n = 5 × 4 n − 2 3 a_n=\cfrac{5 \times 4^n-2}{3} an=35×4n2是推出来的公式,由于 a n a_n an必定是整数,所以 5 × 4 n − 2 3 \cfrac{5 \times 4^n-2}{3} 35×4n2也必是整数,所以 5 × 4 n − 2 5 \times 4^n-2 5×4n2必是3的倍数。取模过程中,如果分子直接对mod取模,可能由于取模造成分子不是3的倍数而造成 W r o n g A n s w e r Wrong Answer WrongAnswer。所以需要处理。
几个常识性的公式:
( a + b ) % c = ( ( a % c ) + ( b % c ) ) % c (a+b)\%c=((a\%c)+(b\%c))\%c (a+b)%c=((a%c)+(b%c))%c 防止溢出
( a − b ) % c = ( ( a % c ) − b + c ) % c (a-b)\%c=((a\%c)-b+c)\%c (ab)%c=((a%c)b+c)%c 加c, 防止出现负数
( a × b ) % c = ( ( a % c ) × ( b % c ) ) % c (a \times b) \%c =((a\%c) \times (b\%c)) \%c (a×b)%c=((a%c)×(b%c))%c 防止溢出

处理方式①:逆元
算法思路

逆元公式处理分母:
a b % c = ( a × b c − 2 ) % c \frac{a}{b}\%c=(a \times b^{c-2})\%c ba%c=(a×bc2)%c

代码实现
ll niyuan(ll a,ll b,ll c){   
    return a*qpow(b,mod-2,mod)%mod;
}
ll solve1(ll x)
{
	return niyuan(5 * qpow(4, x, mod) - 2 + mod, 3, mod) % mod;
}
处理方式②:不采用逆元
算法思路

a 3 % m o d = a % ( m o d × 3 ) m o d \cfrac{a}{3}\%mod ={\cfrac{a\%(mod \times 3)}{mod}} 3a%mod=moda%(mod×3)
解释: 5 × 4 n − 2 5 \times 4^n-2 5×4n2必是 3 3 3的倍数, 为了最后分子能被 3 3 3整除,那么在快速幂取余的过程中,分子对 3 × m o d 3 \times mod 3×mod取余,这样可以保证快速幂算出来的结果可以被 3 3 3整除

代码实现
ll solve2(ll x)
{
	return (5 * qpow(4, x, 3 * mod) - 2 + 3 * mod) / 3 % mod;
}

算法2:矩阵快速幂

算法思路

理论基础: 矩阵相乘满足结合律
不构造等比数列,构造矩阵,用矩阵快速幂来做:
因为:
[ 4 1 0 1 ] × [ a n − 1 2 ] = [ 4 × a n − 1 + 2 2 ] = [ a n 2 ] \left[ \begin{matrix} 4 &1 \\0 &1\end{matrix} \right] \times \left[ \begin{matrix} a_{n-1} \\2 \end{matrix} \right]=\left[ \begin{matrix} 4 \times a_{n-1}+2 \\2\end{matrix} \right]=\left[ \begin{matrix} a_n \\2 \end{matrix} \right] [4011]×[an12]=[4×an1+22]=[an2] a 0 = 1 a_0=1 a0=1
所以:
[ a n 2 ] = [ 4 1 0 1 ] n × [ 1 2 ] \left[ \begin{matrix} a_n \\2 \end{matrix} \right] = {\left[ \begin{matrix} 4 &1 \\0 &1\end{matrix} \right]}^{n} \times \left[ \begin{matrix} 1 \\2 \end{matrix} \right] [an2]=[4011]n×[12]

代码实现
struct matrix
{
	ll num[2][2];
	int n, m;
	struct matrix operator*(struct matrix b)
	{
		struct matrix c;
		c.n = n;
		c.m = b.m;
		for (int i = 0; i < c.n; i++)
		{
			for (int j = 0; j < c.m; j++)
			{
				c.num[i][j] = 0;
			}
		}
		for (int i = 0; i < n; i++)
		{
			for (int j = 0; j < b.m; j++)
			{
				for (int k = 0; k < m; k++)
				{
					c.num[i][j] += num[i][k] * b.num[k][j];
					c.num[i][j] %= mod;
				}
			}
		}
		return c;
	}
};
ll solve(ll x)
{
	struct matrix a, b;
	a.m = a.n = 2;
	a.num[0][0] = 4;
	a.num[0][1] = 1;
	a.num[1][0] = 0;
	a.num[1][1] = 1;
	b.n = 2, b.m = 1;
	b.num[0][0] = 1;
	b.num[1][0] = 2;
	while (x)
	{
		if (x & 1)
		{
			b = a * b;
		}
		x >>= 1;
		a = a * a;
	}
	return b.num[0][0];
}

中档题4-勋总的完美基因

出题人:贝

题目大意

  • 一个环形数组,减一刀,破换成链,使得所以前缀和均大于 0 0 0的位置的数量。

出题报告

前言

  • 出这题就是一个前缀和+思维的题目,但是确实可以被单调队列碾过去,毕竟复杂度都是 O ( n ) O(n) O(n)

测试点数据

  • 来自某场 N O I P NOIP NOIP模拟赛原数据

解题思路

  • 单调队列的话就很裸的维护最小值,破环成链就行了(
  • 这边讲一下前缀和+思维怎么做
  • s u m [ i ] sum[i] sum[i]代表 a [ 1 ] ∼ a [ i ] a[1]\sim a[i] a[1]a[i]的和
  • 我们此处使用的下标,全部都使用原来的旧的下标
  • 如果将一个序列改变为 a k a_k ak开头,则新的前缀和为newSum[k]=a[k]newSum[k+1]=a[k]+a[k+1],以此类推
  • 约定1:我们称,一个下标位置合法,当且仅当newSum[k]>=0
  • 约定2:我们称,一段下标区间合法:当前仅当,这个区间里面所有的下标位置都合法
  • p r e M i n [ i ] preMin[i] preMin[i]代表以 s u m [ 1 ] ∼ s u m [ i ] sum[1]\sim sum[i] sum[1]sum[i]中的最小值
  • s u f M i n [ i ] sufMin[i] sufMin[i]代表以 s u m [ i ] ∼ s u m [ n ] sum[i]\sim sum[n] sum[i]sum[n]中的最小值
  • 要让以 a [ i ] a[i] a[i]开头的序列合法,即原本序列编号的 [ 1 , i − 1 ] [1,i-1] [1,i1]合法,对应条件 p r e M i n [ i − 1 ] + s u m [ n ] − s u m [ i − 1 ] ≥ 0 preMin[i - 1] + sum[n] - sum[i - 1] \geq 0 preMin[i1]+sum[n]sum[i1]0
  • 并且,原来序列编号的 [ i , n ] [i,n] [i,n]合法,即 s u f M i n [ i ] − s u m [ i − 1 ] > = 0 sufMin[i] - sum[i - 1] >= 0 sufMin[i]sum[i1]>=0
  • 同时满足这两个条件即合法,时间复杂度为 O ( n ) O(n) O(n)

代码实现

#include 
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;

int T, n, m;
int a[N];
LL sum[N], sufMin[N], preMin[N];
int main()
{
    cin.tie(0);
    cout.tie(0);
    cin >> n;
    preMin[0] = sufMin[n + 1] = INT_MAX;
    for (int i = 1; i <= n; i ++ ) //下标改为1开始
    {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
        preMin[i] = min(preMin[i - 1], sum[i]);
    }
    for (int i = n; i >= 1; i -- )
        sufMin[i] = min(sufMin[i + 1], sum[i]);
    int ans = 0;
    for (int i = 1; i <= n; i ++ )
    {
        if(sufMin[i] - sum[i - 1] >= 0 && preMin[i - 1] + sum[n] - sum[i - 1] >= 0)
        // 原来下标[i,n]这一段都合法
        // 原来下标[1,i - 1]这一段都合法
            ans ++ ;
    }
    cout << ans << '\n';
}

中档题5-LIS

出题人:曾

题目大意

有序列 A , B A,B A,B,按照 A A A中元素出现的顺序,构造 B B B的最长非递减子序列

出题报告

前言

标题已经提示了:LIS => Longest Increasing Subsequence =>最长递增子序列(实则是最长非递减子序列),本题是PAT真题(PAT甲级真题1045)改编,原题数据友好得多,本题还需要哈希与二分。

测试点详情

共十个测试点:

  • 00测试点: n = m = 10 n=m=10 n=m=10, 可以直接爆搜,每个数选或者不选,复杂度 O ( n × 2 m ) O(n \times 2^{m}) O(n×2m),占 2 2 2
  • 00测试点~05测试点: 1 < = n , m < = 1 0 3 , n × m < = 1 0 6 , m × a n s w e r < = 1 0 6 1<=n,m<=10^3,n \times m <= 10^6, m \times answer<= 10^6 1<=n,m<=103,n×m<=106,m×answer<=106,弱数据,简单的最长非递减子序列能过,占 12 12 12
  • 06测试点~07测试点: n = 4 × 1 0 3 , m = 8 × 1 0 5 n=4 \times 10^3,m=8 \times 10^5 n=4×103,m=8×105, 卡 O ( n × m ) O(n \times m) O(n×m),哈希,占 4 4 4
  • 08测试点~09测试点: n = 3 , m = 8 × 1 0 5 , a n s w e r > = 1 0 4 n=3,m=8 \times 10^5, answer>=10^4 n=3,m=8×105,answer>=104,卡 O ( m × a n s w e r ) O(m \times answer) O(m×answer),二分,占 4 4 4

解题

算法

涉及算法:思维,最长非递减子序列,二分,哈希(或者map)

算法思路1.0
  • 思维:根据元素在序列 A A A出现的顺序,把序列 B B B转成下标
    比如,序列 A A A { 4 , 9 , 2 , 3 } \{4,9,2,3\} {4,9,2,3},序列 B B B { 4 , 2 , 9 , 3 } \{4,2,9,3\} {4,2,9,3},把序列 B B B转成关于下标的序列-> { 1 , 3 , 2 , 4 } \{1,3,2,4\} {1,3,2,4},这样问题就转化成构成最长非递减子序列。
  • 最长非递减子序列:用一个数组 a n s ans ans来存非递减子序列。遍历原序列 B B B时,当元素ans为空或者 B [ i ] B[i] B[i]大于等于 a n s ans ans最后一个元素,将 B [ i ] B[i] B[i] p u s h push push a n s ans ans;当 B [ i ] B[i] B[i]小于 a n s ans ans最后一个元素, 找到 a n s ans ans中第一个大于 B [ i ] B[i] B[i]的数,将之修改成 B [ i ] B[i] B[i]
代码实现1.0
#include 

using namespace std;
int A[4000 + 10], B[800000 + 10];
int main(int argc, char *argv[])
{
	int n, m, i, j;
	cin >> n >> m;
	for (j = 1; j <= n; j++)
		scanf("%d", &A[j]);
	for (i = 1; i <= m; i++)
	{
		scanf("%d", &B[i]);
		for (j = 1; j <= m; j++)
		{
			if (A[j] == B[i])
				break;
		}
		B[i] = j;
	}
	vector<int> ans;
	for (i = 1; i <= m; i++)
	{
		if (ans.size() == 0 || ans[ans.size() - 1] <= B[i])
			ans.push_back(B[i]);
		else
		{
			for (j = 0; j < ans.size(); j++)
			{
				if (ans[j] > B[i])
				{
					ans[j] = B[i];
					break;
				}
			}
		}
	}
	cout << ans.size() << endl;
	return 0;
}
算法思路2.0
  • 在序列 B B B下标转换的过程中,如果采用遍历序列 A A A,那么这个过程的复杂度是 O ( n × m ) O(n \times m) O(n×m) ,06测试点~07测试点会超时,所以需要将序列A制成哈希表(或者直接使用map)。
  • 在更新数组 a n s ans ans的过程中,如果采用遍历数组 a n s ans ans,那么这个过程的复杂度是 O ( m × a n s w e r ) O(m \times answer) O(m×answer) ,08测试点~09测试点会超时,所以在更新数组 a n s ans ans的过程中需要采用二分的方式。
代码实现2.0
#include 

using namespace std;
int A[4000 + 10], B[800000 + 10];
map<int, int> pot;
int main(int argc, char *argv[])
{
	int n, m, i, j;
	cin >> n >> m;
	for (j = 1; j <= n; j++)
	{
		scanf("%d", &A[j]);
		pot[A[j]] = j;
	}
	for (i = 1; i <= m; i++)
	{
		scanf("%d", &B[i]);
		B[i] = pot[B[i]];
	}
	vector<int> ans;
	for (i = 1; i <= m; i++)
	{
		if (ans.size() == 0 || ans[ans.size() - 1] <= B[i])
			ans.push_back(B[i]);
		else
			ans[upper_bound(ans.begin(), ans.end(), B[i]) - ans.begin()] = B[i];
	}
	cout << ans.size() << endl;
	return 0;
}

中档题6-勋总的求偶日记

出题人:贝

题目大意

出题报告

测试点详情

  • 测试点1, T = 10 , 1 ≤ N ≤ 1 0 4 T=10,1\leq N\leq10^4 T=10,1N104
  • 测试点2, T = 10 , 1 ≤ N ≤ 1 0 6 T=10,1\leq N\leq10^6 T=10,1N106
  • 测试点3, T = 10 , 1 ≤ N ≤ 1 0 10 T=10,1\leq N\leq10^{10} T=10,1N1010
  • 测试点4, T = 10 , 1 ≤ N ≤ 1 0 15 T=10,1\leq N\leq10^{15} T=10,1N1015
  • 测试点5, T = 10 , 1 0 14 ≤ N ≤ 1 0 15 T=10,10^{14}\leq N\leq10^{15} T=10,1014N1015

解题思路

状态初始化

  • 状态设计: c [ i ] [ j ] c[i][j] c[i][j]表示长度为 i i i的数字中,有 j j j 1 1 1的数字个数
  • 状态转移:容易得到长度为 i − 1 i-1 i1的数字,在后面加上 0 0 0或者 1 1 1就得到了长度为 i i i的数字的数字个数,即可以得到状态转移方程
  • 状态转移方程: c [ i ] [ j ] = c [ i − 1 ] [ j ] + c [ i − 1 ] [ j − 1 ] c[i][j]=c[i-1][j]+c[i-1][j-1] c[i][j]=c[i1][j]+c[i1][j1]
  • 全放 1 1 1 0 0 0的方案数量为 1 1 1,即 c [ i ] [ 0 ] = c [ i ] [ i ] = 1 c[i][0]=c[i][i]=1 c[i][0]=c[i][i]=1
  • 此处也可以用组合数学中, i i i位选取 j j j i i i的方案数 C i j C_i^j Cij来定义
  • 大多数数位dp题目中,这个过程是数位dp的初始步骤

反向枚举

  • 1 0 15 < 2 50 10^{15}<2^{50} 1015<250我们可以发现 f ( i ) f(i) f(i)的取值也就只有 1 − 49 1-49 149种,如果直接计算最多需要 1 0 15 10^{15} 1015次乘法,必然超时,但是反向枚举 f ( i ) f(i) f(i)的值(有种莫反的感觉),考虑有多少个数的 f f f值等于 k k k,假设有 X k X_k Xk个。这样只需要 49 × l o g X k 49 \times log X_k 49×logXk的时间代价,其中的 l o g X k log X_k logXk快速幂求解 k X k k^{X_k} kXk的复杂度
  • l e n len len n n n的二进制的长度
  • a n s = ∏ i = 1 n f ( i ) = ∏ k = 1 l e n k X k ans=\prod_{i=1}^{n} f(i)=\prod_{k=1}^{len} k^{X_k} ans=i=1nf(i)=k=1lenkXk

如何枚举

  • 那么我们要如何枚举 X k X_k Xk呢?
  • 因为我们枚举的时候,不能超过这个数字 n n n,所以我们需要在枚举的时候判断是否超过上界
  • 设当前还有 n u m num num 1 1 1需要填写, n u m num num的初始值为 k k k
  • 如果 n n n的第 i i i位为 1 1 1,该位置我们填 0 0 0,则接下来的 0 ∼ i − 1 0 \sim i-1 0i1位随便取(怎么填写都不会超),即 i i i个位置里面选择 n u m num num个位置,为 c [ i ] [ n u m ] c[i][num] c[i][num]
  • 如果 n n n的第 i i i位为 1 1 1,该位置我们填 1 1 1,则 n u m − − num-- num,继续迭代处理,直至处理完毕,累加即是最终的 X k X_k Xk

验题问题

  • 验题组有人提出,是否有可能需要欧拉降幂
  • 欧拉降幂的原理为:
  • mod为质数,则有 a b % m o d = a b % ( m o d − 1 ) % m o d a^b\%mod=a^{b\%(mod-1)}\%mod ab%mod=ab%(mod1)%mod
  • (即费马小定理,对分子取模mod-1,因为mod质数,其欧拉函数的值为mod-1,故指数取模的值为mod-1
  • 我给出的答案是不需要的,因为中途不存在 dp数组c[]累加和LL的情况
  • 如何验证?可以使用欧拉降幂提交一发答案,如果是AC说明没有锅,因为数据是通过标程生成的,如果WA来才说明数据有问题。最后使用欧拉降幂的结果是AC,说明不存在中途指数LL的情况)
  • 如果存在爆LL的情况,则需要使用欧拉降幂
  • 即求解时候,涉及到指数的地方,统一取模mod-1

代码实现

  • 迭代 + 组合数学
#include 
using namespace std;

typedef long long LL;

const LL md = 1e9 + 7;

int T;
LL c[65][65], n, ans = 1; //组合数
vector<int> nums;         //二进制分解

LL fpow(LL a, LL b)
{
    a %= md;
    LL res = 1;
    while (b > 0)
    {
        if (b & 1)
            res = res * a % md;
        a = a * a % md;
        b >>= 1;
    }
    return res;
}

void init()
{
    for (int i = 0; i < 64; i++)
    {
        c[i][0] = 1;
        for (int j = 1; j < i; j++)
            c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
        c[i][i] = 1;
    }
}

LL cal(int num) //num个1的方案数
{               //此处取不到n,只计算了每次不取当前位
                //如果每次都不取,这样就取不到n
                //所以此处计算的是
    LL res = 0;
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        if (nums[i])            //当前位置选取0
            res += c[i][num--]; //接下来的位置中随意选取剩下的num个1
        if (num < 0)
            break;
    }
    return res;
}
void dp()
{
    LL tmp = n;
    nums.clear(); //初值
    while (tmp)
    {
        nums.push_back(tmp & 1);
        tmp >>= 1;
    }
    for (int i = nums.size(); i >= 1; i--)
        ans = ans * fpow(i, cal(i)) % md;
    //计算二进制中有i个1的PI
}

int main()
{
    init();
    cin >> T;
    while (T--)
    {
        cin >> n;
        n++; //dp求解的是小于n的,此处加1才为答案
        ans = 1;
        dp();
        cout << ans << '\n';
    }
}
  • 非组合数学做法:记忆化搜索
#include 
using namespace std;

typedef long long LL;
const int B = 66, md = 1e9 + 7;

int T;
LL n, c[B][B], a[B], dp[B][B][2];

LL QuickPow(LL a, LL b)
{
    LL res = 1;
    while (b)
    {
        if (b & 1)
            res = res * a % md;
        a = a * a % md;
        b >>= 1;
    }
    return res;
}

LL dfs(int pos, int left, bool limit)
{//[高位-->低位]==[len - 1, 0],left为剩余需要枚举的1的数量
//limit表示是否受到限制
    if (left == 0)
        return 1;
    if (pos == -1)
        return 0;
    auto &d = dp[pos][left][limit];
    if (d != -1)
        return d;
    d = 0;
    for (int v = 0; v <= (limit ? a[pos] : 1); v ++ )
        d += dfs(pos - 1, left - (v == 1), limit && v == a[pos]);
    return d;
}

LL solve()
{
    LL res = 1;
    int len = 0;
    memset(dp, -1, sizeof dp);
    memset(a, 0, sizeof a);
    while (n)
        a[len++] = n & 1, n >>= 1;
    for (int digit = 2; digit <= len; digit ++)
        res = res * QuickPow(digit, dfs(len - 1, digit, true)) % md;
    //低位为0的适用性更强
    return res;
}

int main()
{
    cin >> T;
    while (T--)
    {
        cin >> n;
        cout << solve() << '\n';
    }
}

中档题7-何老师的课

出题人:杰

题目大意

给定 n n n 个元素,其有两个属性 a i , b i a_i,b_i ai,bi,从中选择若干个元素,使得这些元素的 a a a 属性的最大公约数等于 1 1 1 ,且这些元素的 b b b​ 属性最大值和最小值差值最小。问这个最小值为多少?

出题报告

  • 初衷就是想考察 尺取法+求区间 g c d gcd gcd 的数据结构。
  • 由于 g c d gcd gcd 满足,区间最大, g c d gcd gcd 越容易等于 1 ,因此将元素按 b b b​ 从小到大排序后,刚好可以尺取。

解题思路

  • 方法: 排序+尺取法+线段树
  • 具体描述: 将 n 个元素按 b b b 属性从小到大排序,由于区间 g c d gcd gcd 满足:选择的区间越大, g c d gcd gcd 越容易满足 1 。因此我们可以用尺取法,枚举左端点 str ,求出 g c d gcd gcd​ 等于 1 的第一个右端点 end ,那么 [ s t r , e n d ] [str,end] [str,end] 就是左端点为 str 的满足条件的最小区间。统计最小差值即可。使用线段树时间复杂度: O ( n l o g 2 n ) O(nlog^2n) O(nlog2n),使用 ST 表时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

代码实现

#include
using namespace std;
const int N=5e4+10;

int T=10,tr[N*50],n;

struct node {
	int a,b;
} p[N];
int cmp(node x,node y) {
	return x.b<y.b;
}
int gcd(int a,int b) {
	if(b==0)return a;
	else return gcd(b,a%b);
}
void build(int now,int l,int r) {
	if(l==r) {
		tr[now]=p[l].a;
		return;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	tr[now]=gcd(tr[2*now],tr[2*now+1]);
}
int query(int now,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr)return tr[now];
	int mid=(l+r)/2,ans=0;
    if(ql<=mid)ans=gcd(query(now*2,l,mid,ql,qr),ans);
    if(qr>mid)ans=gcd(query(now*2+1,mid+1,r,ql,qr),ans);
    return ans;
}
void solve(){
	build(1,1,n);
	int end=1,ans=1e9;
    for(int str=1;str<=n;str++){
    	while(end<=n&&query(1,1,n,str,end)!=1)end++;
    	if(end<=n&&query(1,1,n,str,end)==1)ans=min(ans,p[end].b-p[str].b);
	}
	if(ans==1e9)cout<<-1<<endl;
	else cout<<ans<<endl;
}

int main() {
	cin.tie(0);
	cout.tie(0);
	int T;
	cin>>T;
	while(T--) {
		cin>>n;
		for(int i=1; i<=n; i++)cin>>p[i].a;
		for(int i=1; i<=n; i++)cin>>p[i].b;
		sort(p+1,p+n+1,cmp);
		solve();
	}
	return 0;
}

中档题8-回文串判断

出题人:弛


题目大意

n个字符串自由组合(可以和自己组合)问有几种结果是回文串

出题报告

  • 前言:第一个测试点靠理解题意,第四个测试点题目有描述***不同行的字符串可能相同***奇思妙想一下加上去重可以水过去,这8分比较好拿,第二个测试点的规律需要想一想,多试几个回文串的情况可以找到。

    数据比较水所以很大一部分不涉及算法,想到解法暴力实现就可以。

  • 测试点详情

    第一个测试点: 常规数据,每串的长度小于20,字符串个数小于10,字符串是回文串,可以暴力通过,理解题意即可。

    第二个测试点: 回文串版本的大数据,字符串个数大于1e5,字符串长度小于17 ,需要找到回文串的规律,暴力超时。

    第三个测试点:非回文串的大数据,字符串个数1e4,字符串长度小于10,普遍情况,需要正解。

    第四个测试点:非回文串的大数据,字符串个数小于1e4,但是相比上个测试点,有大量重复数据,可以通过去重减少字符串个数,然后暴力求解。

解题思路

  • 回文串版本:先说结论,当且只当a、b两个回文串的最小循环节一样时,a+b是回文串。

    证明:假设a、b都由回文串s构成,a是x个s构成,b是y个s构成,ab拼接在一起就是x+y给s构成,此时ab的第一个s和第x+y的s对应,依次类推,如果x+y是偶数只是ssss的形式,每个s都对应最后结果仍是一个回文串。如果x+y是奇数,除了中间这个s其他s都是一一对应,中间这个s因为本身就是回文串,所以最后结果仍然是一个回文串。

​ 暴力找到每个回文串的最小循环节后,循环节相同的拼接就是答案。

  • 常规解法:两个字符串(a,b => ab)拼成一个回文串。情况一:a比b短,a的反串是b的后缀,同时b的剩余部分是一个回文串(比如:xyz和abazyx,xyz是zyx的反串,aba是回文串)。情况二:a,b一样长,a的反串就是b。情况三:a比b长,b的反串是a的前缀,同时a的剩余部分是回文串。

    情况一和三,在判断的时候需要知道字符串的前缀或者后缀是不是一个回文串,所以先预处理出每个字符串前缀或者后缀是回文串的位置。

    方法一:可以使用kmp算法,将字符串本身和他的反串进行匹配,匹配的结果就是回文串

    方法二:马拉车算法,标记前缀或者后缀是回文串的位置

    然后问题就变成字符串匹配了,这种多字符串匹配问题使用字典树。先把所有串放入字典树,字典树的每个节点多加一个参数就是从当前位置开始剩下部分是不是回文串,然后拿所有反串跑字典树,跑到叶子节点就看当前位置是不是回文后缀。如果字符串匹配完了,就看当前节点后面有几个回文串

    细节部分比较多。标程为了防止内存溢出,把所有字符串存在了一个数组里面,然而出数据的时候长字符串的数据不好出,就没有出很长的字符串,最多才100,所以其实可以把所有字符串分开来存,不需要存在一起。

代码实现

#include
using namespace std;
#define ll long long
const int N = 2000010;
char s[N];
char ss[2 * N];
int len, cnt;
int p[N * 2];
int sum[N];
bool pre[N], suf[N];
int tot;
void init(char* s) {//将每两个字符中插入一个字符
    cnt = 1;
    ss[0] = '!'; ss[cnt] = '#';
    for (int i = 0; i < len; i++) {
        ss[++cnt] = s[i], ss[++cnt] = '#';
    }
    ss[cnt + 1] = '#';
    ss[cnt + 2] = '#';
}
void manacher(int x) {
    int pos = 0, mx = 0;
    for (int i = 1; i < cnt; i++) {
        if (i < mx) p[i] = min(p[pos * 2 - i], mx - i);
        else p[i] = 1;
        while (ss[i + p[i]] == ss[i - p[i]]) p[i]++;
        if (mx < i + p[i]) mx = i + p[i], pos = i;
        if (p[i] == i)
            pre[tot + p[i] - 2] = 1;  //前缀回文
        if (p[i] == cnt - i + 1)
            suf[tot + sum[x] - p[i] + 1] = 1;  //后缀回文
    }
}
int res = 1;
struct node {
    int flag;
    int nex[27];
    int v;//从当前开始是不是回文串
    int num;
}trie[N];
void add(char* s) {
    int now = 1;
    for (int i = 0; i < len; i++) {
        int x = s[i] - 'a';
        if (trie[now].nex[x] == 0) {
            trie[now].num++;
            res++;
            trie[now].nex[x] = res;
            now = res;
            if (suf[tot + i]) {
                trie[now].v++;
            }
        }
        else {
            now = trie[now].nex[x];
            if (suf[tot + i]) {
                trie[now].v++;
            }
        }
    }
    trie[now].flag++;
}
ll find(char* s) {
    int now = 1;
    int i;
    ll extr = 0;
    for (i = len-1; i >=0; i--) {
        if (pre[i + tot])extr += trie[now].flag;
        int x = s[i] - 'a';
        if (trie[now].nex[x] == 0)return extr;
        now = trie[now].nex[x];
    }
    if (i == -1) {
        for (int j = 0; j < 26; j++) {
            extr += trie[trie[now].nex[j]].v;
        }
        extr += trie[now].flag;
    }
    return extr;
}
int main() {
    int T,n;
    scanf("%d",&T);
    for(int p=0;p<T;p++){
        scanf("%d",&n);
        for (int i = 0; i <= res+2; i++) {
            trie[i].flag = trie[i].num = trie[i].v = 0;
            memset(trie[i].nex, 0, sizeof(trie[i].nex));
        }
        res = 1;
        memset(pre,0,sizeof(pre));
        memset(suf,0,sizeof(suf));
        tot = 0;
        for (int i = 1; i <= n; i++) {
            scanf("%d %s", &sum[i], s + tot);
            len = sum[i];
            init(s + tot);
            manacher(i);
            len = sum[i];
            add(s + tot);
            tot += sum[i];
        }
        ll an = 0;
        tot = 0;
        for (int i = 1; i <= n; i++) {
            len = sum[i];
            an += find(s + tot);
            tot += sum[i];
        }
        printf("%lld\n", an);
    }
}

压轴题

压轴题1-贝贝的数组划分

出题人:贝

题目大意

  • 大意,将一个 n n n个元素的数组划分为 k k k个子数组(元素下边连续,且不为空),每个子数组的值为其中所有元素的值的和。求所有子数组的值的按位与的最大值。

出题报告

测试点详情

  • 测试点1, T = 5 , n = 10 , 1 ≤ a i ≤ 127 T=5,n=10,1\leq a_i\leq127 T=5,n=10,1ai127 10 10 10组测试用例 k k k依次为 1 − 10 1-10 110
  • 测试点2, T = 10 , n = 10 , 1 ≤ a i ≤ 127 T=10,n=10,1\leq a_i\leq127 T=10,n=10,1ai127 10 10 10组测试用例 k k k依次为 1 − 10 1-10 110
  • 测试点3, T = 10 , n = 50 , 1 ≤ a i < 2 50 T=10,n=50,1\leq a_i< 2^{50} T=10,n=50,1ai<250 10 10 10组测试用例 k k k依次为 1 − 10 1-10 110
  • 测试点4, T = 10 , n = 50 , 1 ≤ a i < 2 50 T=10,n=50,1\leq a_i< 2^{50} T=10,n=50,1ai<250,第 i i i组测试用例 k k k 5 i 5i 5i
  • 测试点5, T = 10 , n = 50 , 2 25 ≤ a i < 2 50 T=10,n=50,2^{25}\leq a_i< 2^{50} T=10,n=50,225ai<250,第 i i i组测试用例 k k k 5 i 5i 5i

解题思路

  1. 总体思路为:贪心+区间 D P DP DP+位运算+前缀和+二分

  2. 设最后的答案为ans

  3. 区间 D P DP DP状态设计+位运算思想:当前验证res是否为ans的二进制的前若干位,f[i][j]表示[1,i]区间划分成为j份,是否存在解,如果存在则为true,否则为false。特殊的有(根据状态含义),f[0][j]等于false, 1 ≤ j ≤ n 1\leq j \leq n 1jn,且初始f[0][0]等于true

  4. 贪心+二分思想: 从二进制的高位往低位贪心,二分ans当前位的结果,check()函数的返回值为1,说明ans当前位为1,否则为0

  5. 区间 D P DP DP决策:显然,将[1,i]分为j份,的前一个状态就是由另一个区间分解为j-1份得来的,即[1,k],其中 0 ≤ k < i 0\leq k< i 0k<i,然后再与[k+1,j]区间和按位与,若按位于的结果中包含当前的res,则即可由f[k][j-1]转移到f[i][j]

  6. 区间 D P DP DP状态转移方程:易得只要[1,k]中存在一种方案使得其为true则最后的f[i][j]就为true(其中 0 ≤ k < i 0\leq k< i 0k<i),所以用的就是的性质。状态转移方程为:f[i][j] = f[i][j] | f[k][j-1]

  7. 区间 D P DP DP转移顺序:虽然这边我已经用字母表的顺序标记出来,三层for循环的顺序应该为i-j-k,但是如果现场推的时候,你设计的变量不会刚刚好是这三个,考虑转移的顺序,也往往容易再众多 d p dp dp教程中被忽略掉。所有的f[k][j-1]都应该在f[i][j]之前被计算出来,并且 0 ≤ k < i 0\leq k < i 0k<i,所以k受到i的限制,ki内层循环。并且j受到i的限制,你分的份数不能超过子数组的长度,所以j也在i的内层循环。jk相互不限制,所以转移的循环顺序i-j-ki-k-j都是可行的。

  8. 1LL<这样写才不会爆LL范围,最大为1LL<<50。但是中间应用到了sum和,n 50 50 50,所以从 60 60 60开始保险(貌似我数据还没改,等我没有那么忙了就加一下)

代码实现

#include 
using namespace std;

typedef long long LL;
const int N = 55;

int T, n, m;
LL a[N], sum[N];
bool f[N][N];

bool check(LL res) //二分思想
{
    memset(f, 0, sizeof f);
    f[0][0] = true;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i and j <= m; j++) //分成j份
            for (int k = 0; k < i; k++)
                if ((sum[i] - sum[k] & res) == res) //[k+1,i]这个区间是否满足sum[k+1,i]&res=res
                    f[i][j] |= f[k][j - 1];         //f[k][j-1]存在的情况才可以转移过去,只有0,1的dp
    return f[n][m];
}

int main()
{
    cin.tie(0);
    cout.tie(0);
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            cin >> a[i], sum[i] = sum[i - 1] + a[i];
        LL res = 0;
        for (int i = 60; i >= 0; i--) //高位到低位贪心
        {
            res |= 1LL << i;
            if (!check(res))
                res ^= 1LL << i;
        }

        cout << res << '\n';
    }
    return 0;
}

压轴题2-别装 13

出题人:杰


题目大意

给定 n n n 个点的一棵树,每个结点最多可以放累计高度为 12 12 12 的弹珠,初始时每个点的累计高度值均为 0 0 0 m m m 次操作,每次操作给定 x i , y i , h i x_i,y_i,h_i xi,yi,hi ,如果从 x i x_i xi y i y_i yi 的简单路径上有可以放入 h i h_i hi 的点,则第一个可以放入的点累计高度加上 h i h_i hi m m m 次操作后,求每个结点累计了多少高度的弹珠。

出题报告

  • 初衷是为了考查树链剖分套线段树。树上路径修改想到树链剖分,第一个满足条件的点,想到线段树二分,于是剩下的就是敲模板了
  • O ( n l o g 2 n ) O(nlog^2n) O(nlog2n) 的时间复杂度。但是高度才12,总感觉可以有树上瞎搞的方法可以做,希望赛场上看见大佬表演别的方法!!!

解题思路

暴力送10分

  • 对于 x , y x,y x,y 这条路径,直接跑 x x x l c a lca lca,找到第一个满足的点;没有的话,再跑 y y y l c a lca lca​ ,找到最后一个满足的点即可。

  • 以下为部分代码,前置知识是 l c a lca lca

void solve(int x,int y,int w){
	int t=-1;
	while(x!=y){
		if(dep[x]>=dep[y]){//x往上跳 
			if(ans[x]+w<=12){
				ans[x]+w;
				return;
			}
			x=f[x];
		}else{//y往上跳 
			if(ans[y]+w<=12)t=y;
			y=f[y];
		}
	}
	if(ans[x]+w<=12){//lca位置 
		ans[x]+=w;
		return;
	}
	if(t!=-1)ans[t]+=w;
}

树链剖分+线段树二分

  • 树链剖分部分:

  • 树链剖分之后,任意两个点的路径均在 l o g n logn logn 个线段树上的区间内了。

  • 每次给定 x , y x,y x,y​ ,我们将这 l o g n logn logn​ 个区间离线,依次对每个线段树二分,找到第一个满足的点即可。

  • 但是需要注意的是,如果是从 x x x​​ 往上跑重链的话,应该是在线段树上该重链所对应区间,靠右二分。如果是从 y y y​​​ 往上跑重链的话,应该是在线段树上该重链所对应区间,靠左二分。

  • 因此对于是 x x x 往上跳重链的区间,我们记录在 q 1 q1 q1 中;对于是 y y y 往上跳重链的区间,我们记录在 q 2 q2 q2 中​。

  • 最后正着跑 q 1 q1 q1 中的区间,线段树靠右二分找到第一个满足条件的点。找不到就倒着跑 q 2 q2 q2​ 中的区间,线段树靠左二分找到第一个满足条件的点。

  • 线段树二分部分:

  • 线段树建树时,将左右结点初始化成 12 ,并维护区间最大值(表示,对于一个弹珠发射,该区间最多可以放多少高度的弹珠),那么区间最大值是否 > = h i >=h_i >=hi 就表示区间内是否有满足条件的点。

  • 如果是靠左二分:就优先判断作左区间是否有满足条件的点,有就递归到左子区间查,没有就判断右区间是否有满足条件的点,有就递归右子区间查,还是没有就表示整个区间内没有满足条件的点了。

代码实现

#include
using namespace std;
const int N=1e6+10;

struct E {
	int u,v,next;
} e[N*2];
int vex[N],tot,dep[N],id[N],seg[N],cnt,top[N],f[N],tr[N*40],siz[N],son[N],n,m;
struct Q {
	int l,r;
} q1[N],q2[N];

void add(int u,int v) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].next=vex[u];
	vex[u]=tot;
}
void build(int now,int l,int r) {
	if(l==r) {
		tr[now]=12;
		return;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	tr[now]=max(tr[now*2],tr[now*2+1]);
}
int query(int now,int l,int r,int ql,int qr,int w,int key) {//key=1,表示靠右二分;key=2,表示靠左二分 
	if(l==r) {
		if(tr[now]>=w) {
			tr[now]-=w;
			return 1;
		} else return 0;
	}
	int mid=(l+r)/2,t=0;
	if(ql<=l&&r<=qr) {
		if(key==1) {
			if(tr[now*2+1]>=w) {
				t=query(now*2+1,mid+1,r,ql,qr,w,key);
				tr[now]=max(tr[now*2],tr[now*2+1]);
				return t;
			} else {
				t=query(now*2,l,mid,ql,qr,w,key);
				tr[now]=max(tr[now*2],tr[now*2+1]);
				return t;
			}
		} else {
			if(tr[now*2]>=w) {
				t=query(now*2,l,mid,ql,qr,w,key);
				tr[now]=max(tr[now*2],tr[now*2+1]);
				return t;
			} else {
				t=query(now*2+1,mid+1,r,ql,qr,w,key);
				tr[now]=max(tr[now*2],tr[now*2+1]);
				return t;
			}
		}
	}
	if(key==1) {
		if(qr>mid)t=query(now*2+1,mid+1,r,ql,qr,w,key);
		tr[now]=max(tr[now*2],tr[now*2+1]);
		if(t==1)return 1;
		if(ql<=mid)t=query(now*2,l,mid,ql,qr,w,key);
		tr[now]=max(tr[now*2],tr[now*2+1]);
		if(t==1)return 1;
		else return 0;
	} else {
		if(ql<=mid)t=query(now*2,l,mid,ql,qr,w,key);
		tr[now]=max(tr[now*2],tr[now*2+1]);
		if(t==1)return 1;
		if(qr>mid)t=query(now*2+1,mid+1,r,ql,qr,w,key);
		tr[now]=max(tr[now*2],tr[now*2+1]);
		if(t==1)return 1;
		else return 0;
	}
	tr[now]=max(tr[now*2],tr[now*2+1]);
}
int queryvalue(int now,int l,int r,int x) {//单点查询 
	if(l==r)return tr[now];
	int mid=(l+r)/2;
	if(x<=mid)return queryvalue(now*2,l,mid,x);
	else return queryvalue(now*2+1,mid+1,r,x);
}
void predfs(int u,int fa) {//树链剖分预处理第一遍dfs 
	f[u]=fa;
	dep[u]=dep[fa]+1;
	siz[u]=1;
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa)continue;
		predfs(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])son[u]=v;
	}
}
void redfs(int u,int fa,int root) {//树链剖分预处理第二遍dfs 
	id[u]=++cnt;
	seg[cnt]=u;
	top[u]=root;
	if(son[u]!=0)redfs(son[u],u,root);
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(v==fa||v==son[u])continue;
		redfs(v,u,v);
	}
}
void solve(int x,int y,int w) {
	int key=1;//key=1,表示是最初的x在往上跳重链; key=2,表示是最初的y在往上跳重链;
	int top1=0,top2=0;
	while(top[x]!=top[y]) { //不在一条重链上
		if(dep[top[x]]<dep[top[y]]) {//x为所在重链较深的点
			swap(x,y);
			key=-key;
		}
		if(key==1) {
			top1++;
			q1[top1].l=id[top[x]];
			q1[top1].r=id[x];
		} else {
			top2++;
			q2[top2].l=id[top[x]];
			q2[top2].r=id[x];
		}
		x=f[top[x]];
	}
	if(dep[x]<dep[y]) {
		swap(x,y);
		key=-key;
	}
	if(key==1) {
		top1++;
		q1[top1].l=id[y];
		q1[top1].r=id[x];
	} else {
		top2++;
		q2[top2].l=id[y];
		q2[top2].r=id[x];
	}
	for(int i=1; i<=top1; i++) {
		int t=query(1,1,n,q1[i].l,q1[i].r,w,1);
		if(t==1)return;
	}
	for(int i=top2; i>=1; i--) {
		int t=query(1,1,n,q2[i].l,q2[i].r,w,2);
		if(t==1)return;
	}
}
int main() {
	//freopen("5.in","r",stdin);
	//freopen("5.out","w",stdout);
    cin.tie(0);
    cout.tie(0);
	int u,v,w;
	cin>>n>>m;
	for(int i=1; i<n; i++) {
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	predfs(1,0);
	redfs(1,0,1);
	build(1,1,n);
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>w;
		solve(u,v,w);
	}
	for(int i=1; i<=n; i++)cout<<12-queryvalue(1,1,n,id[i])<<'\n';
	return 0;
}

你可能感兴趣的:(比赛题解,算法,动态规划,概率论)