河南萌新联赛2024第(四)场:河南理工大学

迟来的更新,从16号开始办了点个人私事,19号才开更新这篇文章。

AAAAA

 这题提交准确率有点低,下面是答案代码和解析,自行斟酌。

这段代码的目的是解决一个图论问题,主要涉及寻找一种最优的分割方式,以使得图的某种属性最小化。这种问题在图论中比较常见,通常涉及深度优先搜索(DFS)和最小割、桥等概念。

#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
using i128 = __int128;
constexpr int inf = 2E9;
constexpr i64 inf64 = 1E18;
constexpr i128 inf128 = 1E27;

const int N = 2e5 + 10;
vector<pair<int,int>> g[N];
int a[N];

int dep[N]; 
int low[N];   
i128 sz[N];  

i128 cnt, root;

void print(__int128 x){//由于标准库不支持直接输出 __int128,所以需要这样逐位打印。
    if(x < 0){
        cout << '-';
        x = -x;
    }
    if(x > 9)
        print(x / 10);
    cout<<char(x % 10 + '0');
}

void dfs1(int u, int pa)//这个 DFS 同时标记了两种类型的边:back edge 和 forward edge。
{
    low[u] = inf;//low[u] 用来标识当前节点或子树能够到达的最小深度节点。
    sz[u] = a[u];//sz[u] 是当前节点为根的子树权值之和。
    // 更新节点的深度
    if(pa == -1) dep[u] = 1;
    else dep[u] = dep[pa] + 1;
    for(auto& [v, flag] : g[u])
    {
        if(dep[v] > 0)
        {
            if(v == pa) continue;
            // 标记所有 back edges 和 forward edges
            if(dep[v] < dep[u])
            {
                flag = 1;
                low[u] = min(low[u], dep[v]);
            }
            else
            {
                flag = 2; 
            }
            continue;
        }
        // 更新子树的权重和
        dfs1(v, u);
        low[u] = min(low[u], low[v]);
        sz[u] += sz[v];
    }
}

void dfs2(int u, int pa)//dfs2用来计算将一个图分割成两部分后,所能得到的最小 res 值。
{
    i128 part = sz[root] - sz[u];//part 是除了当前子树外的其他部分的大小。
    i128 res = 0;//res 是分割后两个部分的平方和,cnt 用来记录全局最小值。
    for(auto [v, flag] : g[u])
    {
        if(flag != 0 || v == pa) continue;
        dfs2(v, u);
        if(low[v] < dep[u])
        {
            //low[u] = min(low[u], low[v]);
            part += sz[v];
        }
        else
        {
            res += sz[v] * sz[v];
        }
    }
    res += part * part;
    cnt = min(cnt, res);
}

void solve()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        g[u].push_back({v, 0});
        g[v].push_back({u, 0});
    }
    for(int i = 1; i <= n; i++) cin >> a[i];
        
    i128 sum = 0;
    i128 dlt = 0;
    for(int i = 1; i <= n; i++)
    {
        if(dep[i]) continue;
        cnt = inf128;
        root = i;
        dfs1(i, -1);
        dfs2(i, -1);
        sum += sz[i] * sz[i];
        dlt = max(dlt, sz[i] * sz[i] - cnt);
    }
    print(sum - dlt);//其中 sum 是所有子树权值的平方和,而 dlt 是分割后减少的最大值。
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    solve();
}

BBBBB

​
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
bool st[N];
int a[N];
int n,m;

signed main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    sort(a+1,a+n+1);
    int ans=0;
    for(int i=1;i<n;i++){
        ans=max(ans,((1<<m)-1)^a[i]^a[i+1]);//看下面的具体解析
    }
    cout<<ans;
    return 0;
}

​

就这个题而言,任意两个数中,找同或运算后结果最大的结果,那怎么找呢?

1.我们需要使这两个数在二进制表示上尽可能相似。因为同或操作的本质是,两个位相同结果为1,不同则为0。因此,要使同或操作的结果最大,应该使两数之间的二进制位差异尽可能小,有三种解决办法:

1.选择相近的数值:选择两个相近的数,这样它们的二进制表示在大多数位上都将相同。

2.特殊情况考虑:如果有特殊情况要处理,比如说范围限制或其他操作条件,可能需要根据具体情况调整选择。

3.位操作优化:在某些情况下,可以通过位操作(如位移)来人为调整数值,使得它们的同或结果更大。

2.问题来了,对于所有的整数而言那是不是两个整数越接近同或结果就越大?

当然不能以偏概全。

 3.让我们来解析一下这串核心代码ans=max(ans,((1<<m)-1)^a[i]^a[i+1]);

 简单解释一下:

1.位移操作 1<<m 是在二进制中将数字 1 向左移动 m 位。在二进制中,每向左移动一位实际上是将该数字乘以2。例如: 1(二进制为 0001)向左移3位,得到的结果就是1*2^3=8。

2.低m位,拿第三位来解释一下,指的是这个数在二进制表示中最右边的三个位。这三个位对应于这个数最小的三个权值位。例如,一个二进制数101101的低三位是101,它们分别代表着2^0、2^1和2^2位,也就是1、2和4的位置。

3.关注低位的目的,也就是只保留我们关注的,运算过程中实际用到的部分,关注一个数的低位(low-order bits)通常是因为在进行位操作时,我们可能只对这些位感兴趣,或者这些位在算法中具有特定的功能。例如,在某些加密算法中,可能只会使用数字的低几位来进行某些运算。 

4.那么问题又双叒叕来了,设置这个掩码的目的是什么呢? 

 CCCCC

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NUM = 998244353;
const ll N = 2e5 + 10;
ll n, m;
ll jc[N];
//快速幂和费马小定理我在第一篇博客中介绍过,比较详细可自行翻阅
void init() {
    jc[0] = 1;
    for (ll i = 1; i < N; i++) {//用一个很大的阶乘数组存放数据,而非i<n
        jc[i] = i * jc[i - 1] % NUM;
    }
}

ll qsm(ll a, ll b) {
    ll ans = 1;
    while (b) {
        if (b & 1) {
            ans = (ans * a) % NUM;
        }
        a = (a * a) % NUM;
        b >>= 1;
    }
    return ans;
}

ll C(ll a, ll b) {
    if (a < b) return 0;
    return jc[a] * qsm(jc[b], NUM - 2) % NUM * qsm(jc[a - b], NUM - 2) % NUM;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m;
    vector<ll> a(n);
    for (ll i = 0; i < n; i++){
        cin>>a[i];
        m-=a[i];
    }
    init();
    ll sum = 0;
    for(ll i=0;i<=m;i++){
        sum=(sum+C(i+n-1,n-1)%NUM)%NUM;//这步代码下面有详解
    }
    cout << sum << endl;
    return 0;
}

 我们现在来看一下这个关键一步的代码:

当然这个题还可以用DP思想求解

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=10100;
ll a[N],dp[N];
ll n,m;
int p=998244353;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		a[i]+=a[i-1];
//计算其前缀和 a[i]。前缀和的含义是:到第 i 个岗位为止,总共需要的最小志愿者数。
	}
	m-=a[n];//计算剩余志愿者数量 m
	dp[0]=1;//当没有志愿者要分配时,只有一种方案(即什么都不做)
	for(int i=0;i<=n;i++)//i 代表正在处理的岗位数
	{
		for(int j=0;j<=m;j++)//j 代表剩余志愿者的数量
		{
			dp[j]+=dp[j-1];
//通过累计前一个状态的结果来计算当前状态的可能分配方案数
			dp[j]%=p;//避免数据过大
		}
	}
	cout<<dp[m]%p;
	return 0;
}

 DDDDD

#include<iostream>
#include<cmath>
using namespace std;
bool isPrime(long long int x){
    if(x==1||(x%2==0&&x!=2))
        return false;
    else if(x==2||x==3)
        return true;
    else{
        for(long long int j=3;j<sqrt(x);j++){
            if(x%j==0){
                return false;
                break;
            }
        }
        return true;
    }
}
int main(){
    typedef long long int ll;
    ll t;
    cin>>t;
    ll x;
    ll i=0;
    for(i=1;i<=t;i++){
        cin>>x;
        if(isPrime(x))
            cout<<"Yes"<<endl;
        else cout<<"No"<<endl;
    }
    return 0;
}

EEEEE

进阶版写法(埃拉托色尼筛法,upper_bound,lower_bound)

#include<bits/stdc++.h>
using namespace std;
#define LL int
const LL N=1e8+5;
LL f[N],cnt=0;
//f[N] 用来存储所有筛选出来的质数。cnt 是质数个数的计数器。
bool v[N];
//用于标记是否为质数,v[i] 为 true 表示 i 不是质数

/*旧版
  bool isPrime(ll n) {//不可直接使用,因为不属于库函数
  if (n <= 1) return false;
  if (n == 2 || n == 3) return true;
  if ((n % 2 == 0&&n!=2) || (n % 3 == 0&&n!=3)) return false; 
  for (int i = 5; i * i <= n; i += 6) {
  if (n % i == 0 || n % (i + 2) == 0) return false;
  }
  return true;
  }
 *///下面是进阶版,运用埃拉托色尼筛法,这个拓展点问题看代码下面的详解
void kss(){
	for(int i=2;i<=N;i++){
		if(!v[i])
			f[cnt++]=i;
//如果 i 是质数(即 v[i] 为 false),则将其存入 f 数组中。
		for(int j=0;j<cnt&&f[j]*i<=N;j++){//用来标记所有 i 的倍数为非质数。
			v[f[j]*i]=1;
			if(i%f[j]==0)
				break;
//如果 i 是 f[j] 的倍数,那么跳出内层循环,因为更大的倍数已经由之前的数标记过了。
		}
	}
}
int main(){
	LL T,x,y,ans;
	kss();
	cin>>T;
	while(T--){
		cin>>x>>y;
		ans=upper_bound(f,f+cnt,y)-lower_bound(f,f+cnt,x);
//lower_bound 用于查找给定元素在有序范围内的第一个不小于(即大于或等于)目标值的位置。
//upper_bound 用于查找给定元素在有序范围内的第一个大于目标值的位置。
//两个迭代器相减,得到的就是在 [x, y] 区间内的质数的个数 ans。
		cout<<ans<<" "<<(x==2?max(ans-2,0):0)<<endl;
//(x==2?max(ans-2,0):0)这部分是为了计算题目中要求的第二个值,它处理了当 x 为 2 时的特殊情况。
	}
}

让我们来看看这个进阶版代码有什么可能出现的问题: 

1. 既然不可以直接使用isPrime(),那为啥筛选质数的函数 kss()也没有判断质数的过程就直接继续往后进行了?

函数 kss() 实际上就是通过一种称为 埃拉托色尼筛法(Sieve of Eratosthenes) 的算法来筛选出一定范围内的所有质数。这个函数不需要逐个数地判断某个数是否为质数,而是通过标记的方式直接找到所有质数。由于 kss() 函数已经预先筛选出了一定范围内的所有质数,并将它们存储在数组 f[] 中,因此在处理输入时只需要利用这个数组来快速查询给定区间内的质数个数。

 2.v[N]数组用来储存质数,但是代码中没有任何赋值v[i]的过程,怎么直接就使用了?

3.线性筛法是什么?

说简单直接一点就是,质数的倍数都不是指数,筛掉就行了,下面看线性筛法的详解及伪代码。

void linear_sieve(int N) {
    vector<int> primes;
    vector<bool> is_prime(N + 1, true);  // 假设所有数都是质数
    
    for (int i = 2; i <= N; i++) {
        if (is_prime[i]) {  // 如果当前数是质数
            primes.push_back(i);  // 将其添加到质数列表中
        }
        for (int p : primes) {  // 遍历所有已知的质数
            if (i * p > N) break;  // 如果乘积超出范围,停止
            is_prime[i * p] = false;  // 标记为合数
            if (i % p == 0) break;  // 保证每个合数只被标记一次
        }
    }
}

4.为什么内层循环是从j=0开始的,并且终止条件为什么是j<cnt ?

cnt 记录了在 a[] 数组中已经存储了多少个质数,这个循环的目的是使用所有已经找到的质数去标记当前数 i 的倍数为合数。

5.细节易漏点:在这串代码中,我们事先定义了#define  LL int,所以在结尾三元运算符输出的时候写的是x==2?max(ans-2,0):0,但如果没有事先声明LL为int类型,那么就会编译错误,因为在 C++ 中,整型常量 0 的默认类型是 int。当你没有显式指定 0LL 时,编译器会自动将它处理为 int,与 ans(也是 int 类型)一致。如果你定义的 ans 是 long long 类型,或者你的变量涉及到更大范围的数值(如 long long),那么你必须使用 0LL 来匹配参数类型。否则在某些编译器设置下,可能会触发类型不匹配警告或错误。这时使用 0LL 确保常量的类型是 long long,以防止潜在的类型转换问题。

6.细节易漏点:定义的 v 和 a 数组大小为 N(即 1e8 + 5),这是一个非常大的数组,会导致栈溢出问题。通常情况下,栈内存限制无法支持这么大的数组,解决办法是将这些数组定义为全局变量,或者使用动态分配的方式(如 new 或 vector)。

FFFFF

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii = pair<int, int>;
string s;

signed main()
{
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	deque<int>a;
	cin >> s;
	int sum = 0;
	for (int i = 0; i < s.size(); i++) {
		if (isdigit(s[i])) {
			int x = 0, j = i;
			while (isdigit(s[j])) {
				x = x * 10 + s[j] - '0';
				j++;
			}
			sum += x;
			a.push_back(x);
            i = j - 1;
		}
	}
	sort(a.begin(), a.end(), greater<>());
	while (a.size() > 1) {
		cout << a.front() << '+';
		a.pop_front();
	}
	cout << a.back() << endl << sum;
	return 0;
}

 下面是F题的java代码(考试时候我用的java写的,javaAPI也挺好用的)

​
import java.util.*;
public class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String s = sc.next();
        String[] str = s.split("\\+");//把“+”作为间隔,将s字符串切割成若干子串,并存入数组
        int[] a = new int[str.length];
        int i = 0;
        long sum = 0;
        for (i=0;i<str.length;i++) {
            a[i] = Integer.parseInt(str[i]);//将数字字符串转华为数字
            sum += a[i];
        }
        Arrays.sort(a);//默认升序排列
        System.out.print(a[a.length-1]);
        for (i = a.length-2; i>=0;i--) {  // 仅遍历到str.length
            System.out.print("+" + a[i]);
        }
        System.out.print("\n" + sum);
    }
}

​

GGGGG 

这段代码核心在于通过质数分解以及字符串比较来快速计算字符串的最小循环节长度,并且支持区间字符串的快速替换。线性筛法生成质数列表的使用为代码的效率提供了保障,而 memcmp 函数的使用则简化了字符串比较的实现。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N= 2000010;
int primes[N], cnt;
int st[N];
//使用线性筛法来生成所有小于或等于 n 的质数,并将它们存储在 primes 数组中,同时记录每个合数的最小质因数在 st[] 数组中。
void get_primes(int n)
{
	for (int i = 2; i <= n; i ++ )
	{
		if (!st[i]) 
		{
			primes[cnt ++ ] = i;
			st[i]=i;
		}
		for (int j = 0; primes[j] <= n / i; j ++ )
		{
			st[primes[j] * i] = i;
			if (i % primes[j] == 0) break;
		}
	}
}
signed main()
{
	get_primes(N);
	int n,m;
	cin >> n >>m;
	char s[N];
	cin>>s+1;
	while(m--)
	{
		int o,l,r;
		cin>>o>>l>>r;
		if(o==0)
		{
			char c;
			cin>>c;
			memset(s+l,c,r-l+1);//memset 函数快速实现该操作
		}
		else
		{
			int len=r-l+1;//表示当前区间的长度。
			int ans=len;
			while(len>1)
			{
				int x=ans/st[len];
//在每次循环中检查是否可以找到更短的循环节(利用 st[len] 作为质数分解的最小因数)
				if(!memcmp(s+l,s+l+x,(r-l+1)-x))
//memcmp 函数用于比较两个字符串,如果在区间内可以找到一个符合的子串,则更新 ans 为更短的循环节。
				{
					ans=x;
				}
				len/=st[len];
			}
			cout<<ans<<endl;
		}
	}
}

下面解释一下小伙伴们可能碰到的疑惑点:

1. 为什么要设置st[N]?

st[N] 的作用是用于存储每个数的 最小质因数.

2. 在get_primes()函数中st[i]=i;为什么对于st的赋值是从i=2开始,而不是cnt=0?

为什么 st[i] = i 从 i = 2 开始?

从 i = 2 开始:因为 i = 2 是最小的质数,所有更大的质数也是从 i = 2 开始判断的。对于每一个质数 i,第一次遇到时就将 st[i] 赋值为 i 自身,表明 i 是质数,并且其最小质因数是它自己。

为什么不从 cnt = 0 开始?

 cnt 是用来记录质数的个数和存放质数的位置的计数器,而 st[i] 是用来标记每个数的最小质因数的。cnt 从 0 开始只是意味着我们刚开始没有记录任何质数,而 st[i] 的作用与 cnt 无关,它要针对所有可能的 i 进行判断和标记。 cnt 的递增是在找到新的质数后进行的,而 st[i] = i 的赋值是在确认 i 是质数时进行的。这两个操作是独立的,虽然它们在同一个逻辑过程中执行。

3.int x=ans/st[len];这句代码什么意思? 

HHHHH

#include<iostream>
using namespace std;
int main(){
    int t;
    cin>>t;
    int x,n,i;
    int money;
//一半数量的的恶魔有钱就行了,在这一半的恶魔中,恶魔一人一个金币,剩下的金币全是恶魔老大的就行了
    for(i=1;i<=t;i++){
        cin>>x>>n;
        if(n%2==0){
            money=x-(n/2-1);
        }else{
            money=x-(n/2);
        }
        cout<<money<<endl;
    }
    return 0;
}

IIIII 

运用了一次的DFS

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
int t,x,y,n,a,b,lj[N],sum[N];
//lj[N]标记从 y 出发能到达的节点(即路径上是否包含 y)。
//sum[N]用于存储每个节点的子树大小(从该节点开始能访问到的所有节点的数量)
//e[N] 是一个邻接表,存储图中城市之间的连接关系(存储邻居节点)
vector<int> e[N];
void dfs(int u,int fa){
	sum[u]=1;//从u开始遍历
	for(auto v:e[u]){
		if(v==fa) continue;
//v == fa 这一条件用来检查当前遍历的节点 v 是否是当前节点 u 的父节点 fa。避免重复访问已经访问过的节点
		dfs(v,u);
		sum[u]+=sum[v];
		if(lj[v]) lj[u]=1;
		if(u==x&&lj[v]) sum[u]-=sum[v];
//意味着从 x 出发不能经过 y 到达该子树中的节点。
	}
}

int main(){
	cin >>n >>x >>y;
	for(int i=1;i<=n-1;i++){
		cin >>a >>b;
		e[a].push_back(b);
		e[b].push_back(a);
	}
	lj[y]=1;
	dfs(x,0);
	cout <<1ll*sum[x]*sum[y] <<endl;
//输出 sum[x] * sum[y],表示从 x 出发可以到达的节点数量与从 y 出发可以到达的节点数量的乘积
}

疑问点:

fa没有在代码中定义,怎么可以直接使用,我要是使用father了呢?

fa 代表了当前节点 u 的父节点,尽管 fa 没有在代码中明确地定义为一个全局变量或特定的局部变量,但是它作为 dfs 函数的一个参数存在。

如果使用 father 代替 fa: 您可以将 fa 改为 father 或其他任何有效的变量名,代码逻辑不会受到影响,fa 作为 dfs 函数的参数,是在每次递归调用时由当前节点传递给下一个节点,用于标识当前节点的父节点。您可以使用任何有效的变量名来代替 fa,如 father 或 parent,只要在代码中保持一致即可。

运用了两次的DFS

#include<bits/stdc++.h>
using namespace std;
typedef long long int ll;
ll n,x,y,nx,ny;
const ll N = 3e5+10;
bool v[N];
vector<ll> adj[N];
//两次 DFS(深度优先搜索)目的是为了计算分别从节点 x 和 y 出发能够访问到的节点数,
void DFSX(ll temp){
	if(temp==y) return;
//从城市 x 开始遍历,但如果遇到城市 y,它会停止遍历。
	v[temp]=true;
	for(auto it:adj[temp]){
		if(v[it]) continue;
		nx++;
//nx 是从节点 x 出发可以到达的节点数量,不包括节点 y 及其直接或间接连通的部分
		v[it]=true;
		DFSX(it);
	}
}

void DFSY(ll temp){
	if(temp==x) return;
//从城市 y 开始遍历,并且如果遇到城市 x,会停止遍历。
	v[temp]=true;
	for(auto it:adj[temp]){
		if(v[it]) continue;
		ny++;
//ny 是从节点 y 出发可以到达的节点数量,不包括节点 x 及其直接或间接连通的部分
		v[it]=true;
		DFSY(it);
	}
}

int main(){
	cin>>n>>x>>y;
	for(ll i=1;i<=n-1;i++){
		ll a,b;
		cin>>a>>b;
		adj[a].push_back(b);
		adj[b].push_back(a);
	}
	DFSX(x);
	memset(v,0,sizeof v);
//使用 memset(vis,0,sizeof vis); 将访问标记数组 vis 重置为 0,以便进行第二次 DFS。
	DFSY(y);
	cout<<(n-nx)*(n-ny);
//n - nx 就是 x 无法到达的节点数
//n - ny 就是 y 无法到达的节点数
//这一乘积常用于评估两个节点之间的独立性,或者它们互相阻碍的程度。
	return 0;
}

JJJJJ 

这段代码实现了在一棵树上寻找两个节点的最近公共祖先(LCA),并结合题目中的特殊要求(与斐波那契数列相关的节点访问),最终确定目标节点。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+7;
ll n,q,r,f[N][27],d[N],x,y,a,k,t[N];
vector<ll> dis[N];
void DFS(ll now,ll fa)
{
	d[now]=d[fa]+1;//每个节点的深度
	f[now][0]=fa;//祖先节点
	for(int i=1;i<=19;i++)
	{
		f[now][i]=f[f[now][i-1]][i-1];//记录的是节点 now 的第 2^i 级祖先节点,用于快速找到 LCA。
	}
	for(auto i:dis[now])
	{
		if(i==fa)continue;
		DFS(i,now);
	}
}
ll LCA(ll u,ll v)//LCA 函数用来寻找两个节点 u 和 v 的最近公共祖先。
{
	if(d[u]<d[v])swap(u,v);
	for(int i=19;i>=0;i--)
	{
		if(d[f[u][i]]>=d[v])u=f[u][i];
	//通过对 u 和 v 的深度进行比较,将较深的节点上移,直到两个节点位于同一深度,然后再同时上移直到找到它们的公共祖先。
	}
	if(u==v)return u;
	for(int i=19;i>=0;i--)
	{
		if(f[u][i]!=f[v][i])
		{
			u=f[u][i],v=f[v][i];
		}
	}
	return f[u][0];
}
int main()
{
	cin>>n>>r>>q;
	for(int i=1;i<n;i++)
	{
		cin>>x>>y;
		dis[x].push_back(y);
		dis[y].push_back(x);
	}
	DFS(r,0);
	t[1]=1,t[2]=2;
	for(int i=3;i<=25;i++)
	{
		t[i]=t[i-2]+t[i-1];//构造了一个斐波那契数列 t,用于后续的查询。
	}
	while(q--)
	{
		cin>>a>>k;
		if(t[k]+a>n||k>=25)
		{
			puts("0");
			continue;
		}
		ll ans=a+t[k];
		for(int i=k+1;i<=n&&a+t[i]<=n;i++)
		{
			ans=LCA(a+t[i],ans);//,程序会根据斐波那契数列移动,然后找到最终的LCA节点。
			if(ans==r)break;
		}
		cout<<ans<<"\n";
	}
	return 0;
}

 KKKKK

利用树状数组高效地统计数组中元素的数量,从而能够快速计算满足条件的比赛对数。 这是一种典型的适合竞赛编程的高效算法,尤其适用于处理较大规模的数据。

样例输出6是怎么来的呢?

不可以先排序在算多少个三元组符合题意,因为题干已经要求了,i<j<k,禁军的实力,要求min(Ai,Ak)<=Aj<=max(Ai,Ak),意思就是找一个三元序列,要么是递增的,要么是递减的就OK了。符合题干的序列枚举如下:

8 4 1

8 11 14

8 11 13

4 11 14

4 11 13

14 13 1

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10,N2=100000;
int lh[N],ll[N],rh[N],rl[N],ls[N],rs[N],tree[N],a[N],t,n;
void add(int x,int k){//用来更新树状数组。
	while(x<=N2){
		tree[x]+=k;
		x+=x&-x;
	}
}
int query(int x){//返回到索引 x 的前缀和
	int sum=0;
	while(x>0){
		sum+=tree[x];
		x-=x&-x;
	}
	return sum;
}
void solve(){
	cin >>n;
	for(int i=1;i<=n;i++) cin >>a[i];
	for(int i=1;i<=n;i++){
		ll[i]=query(a[i]);//表示在 i 之前的元素中有多少个比 a[i] 小,
		lh[i]=query(N2)-query(a[i]-1);//表示在 i 之前的元素中有多少个比 a[i] 大
		ls[i]=query(a[i])-query(a[i]-1);//ls[i] 表示在此之前 a[i] 出现的次数
		add(a[i],1);
	}
	memset(tree,0,sizeof tree);//使用 memset 重置 tree 数组。
	long long ans=0;
	for(int i=n;i>=1;i--){
		rl[i]=query(a[i]);
		rh[i]=query(N2)-query(a[i]-1);
		rs[i]=query(a[i])-query(a[i]-1);
		add(a[i],1);
		ans+=1ll*rl[i]*lh[i];
		ans+=1ll*rh[i]*ll[i];
		ans-=1ll*ls[i]*rs[i];
	}
	memset(tree,0,sizeof tree);
	cout <<ans <<endl;
}
int main(){
	cin >>t;
	while(t--){
		solve();
	}
}

 LLLLL

这题准确率有点低,量力而行。

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long dss[5][30],dxj[5][30],dxd[5][30];
string s;
long long f(long long x,long long y){
	long long t=x,ans=1;
	while(y>0){
		if(y%2==1)
		ans=(ans*t)%mod;
		t=t*t%mod;
		y>>=1;
	}
	return ans;
}
int main(){
	long long su=1,sum,a,ny,n;
	cin>>n>>s;
	for(int i=1;i<=n;i++){
		cin>>a;
		ny=f(a,mod-2);
		int u=(i&1),v=((i&1)^1);
		for(int j=0;j<26;j++){
			dss[u][j]=(1-ny)*dss[v][j]%mod;
			dxj[u][j]=(1-ny)*dxj[v][j]%mod;
			dxd[u][j]=(1-ny)*dxd[v][j]%mod;
		}
		int jj=s[i-1]-'a';
		sum=0;
		for(int k=0;k<=jj;k++){
			sum+=dss[v][k];
		}
		dss[u][jj]+=(sum+su)%mod*ny%mod;
		dss[u][jj]%=mod;
		
		sum=0;
		for(int k=jj;k<26;k++){
			sum+=dxj[v][k];
		}
		dxj[u][jj]+=(sum+su)%mod*ny%mod;
		dxj[u][jj]%=mod;
		
		dxd[u][jj]=(dxd[v][jj]*ny+dxd[u][jj]+(su*ny)%mod)%mod;
		
		su*=(1-ny);
		su%=mod;
	}
	long long ans=0;
	for(int i=0;i<26;i++){
		ans+=dss[(n&1)][i]+dxj[(n&1)][i]-dxd[(n&1)][i];
		ans%=mod;
	}
	printf("%lld",(ans+mod)%mod);
}

 

谢谢观看!See you again!

 

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值