[TOC]

2023ECJTU校赛题解

A. FF 想吃糖果

解题思路

题意:给一副无向图,问你最少删除多少个点,且这些不是度最多的点,使得这幅图不连通?

本题两个难点:

1、最小割删边转化删点。

2、不能删掉度最多的点。

首先先不考虑 FF 的技能,先思考这个问题,最少删多少个点使得图不连通?

那么这个题就是一个最小割问题。但是最小割是删除边使得图不连通,而这个题考虑的是点,所以不能直接乱搞。

那么这样需要一个小技巧,割边转割点,因此我们需要拆点

即把一个点拆成两个点,中间连一条边权为 的边。

前一个点作为“入点”,别的点连边连入这里。

后一个点作为“出点”,出去的边从这里出去。

这样,只要我们切断中间那条边,就可以等效于除去这个点了。

以下图(借别人大佬的,侵权联系删除)举例:

图片描述

红色的边边权为 ,黑色的边边权为 ,原点和汇点的内部边权为 ,因为显然这两个点不能删除。

然后我们需要考虑 FF 的技能,FF 会保护联系最多的人,无法删除这些点。如何处理?

考虑那些点入边直接连到出边对应的点。

但是,这样太麻烦了。

因为,首先要存进入的点的编号,还要存出去的点的编号,之后两层循环相连,你以为这就完了?这样的点不止一个,所以你还得多次操作。不建议这么写。

那么这里也有一个小技巧,我们将拆掉的点中间的权值改成 就好了,以上图为例子:

图片描述

这样就完美地解决了这个问题。

或者重新加一条边,权值为 也行。

注意是除了 FFpld 之外联系最多的!

然后只要求一个最小割就好了,利用最大流最小割定理,跑一遍 dinic 就可以通过了。

AC_Code

  • C/C++
#include<iostream>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
#include<queue>
#include<set>
#include<map>
#define int long long
#define fi first
#define se second
#define inf 0x3f3f3f3f
#define INF 1e18
using namespace std;
typedef pair<int, int> PII;

const int N = 3e6 + 10, M = N * 2, Mod = 1e9 + 7;

int n, m, S, T, ans;
int e[M], ne[M], f[M], h[N], idx;
int q[N], cur[N], d[N];
int st[N], g[1010][1010];
int din[N], mxd;

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], f[idx] = c, h[a] = idx++;
    e[idx] = a, ne[idx] = h[b], f[idx] = 0, h[b] = idx++;
}

bool bfs()
{
    memset(d, -1, sizeof(d));
    int hh = 0, tt = 0;
    q[0] = S, d[S] = 0, cur[S] = h[S];
    while(hh <= tt)
    {
        int t = q[hh++];

        for(int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if(d[ver] == -1 && f[i])
            {
                cur[ver] = h[ver];
                d[ver] = d[t] + 1;
                if(ver == T) return true;
                q[++tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if(u == T) return limit;
    int flow = 0;
    for(int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        cur[u] = i;
        int ver = e[i];
        if(d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if(!t) d[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int r = 0, flow;
    while(bfs()) while(flow = find(S, inf)) r += flow;
    return r;
}

void solve()
{
    cin >> n;
    S = 1, T = n;
    for(int i = 1; i <= n; i++)
    	for(int j = 1; j <= n; j++)
    		cin >> g[i][j];
    
    memset(h, -1, sizeof(h));
    for(int i = 1; i <= n; i++)
    {
        st[i] = idx;
        if(i == S || i == T) add(i, i + n, inf);
        else add(i, i + n, 1);
    }

    for(int i = 1; i <= n; i++)
    	for(int j = 1; j < i; j++)
    		if(g[i][j])
    		{
    			add(i + n, j, inf), add(j + n, i, inf);
    			din[i]++, din[j]++;
			}
	
    for(int i = 2; i < n; i++) mxd = max(mxd, din[i]);

    for(int i = 2; i < n; i++)
        if(din[i] == mxd) f[st[i]] = inf;

    int ans = dinic();
    cout << (ans >= inf / 2? -1: ans) << endl;
}
signed main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
//    cin >> T;
    while(T--)
        solve();
    return 0;
}

B. 吃糖果

解题思路

假如我们求到了操作需要的最小次数,我们就可以知道能不能在不超过 k 次吃掉所有糖果。

dp[i][j] 是将 ij 的糖果吃掉需要的最小次数,因为操作的性质是连续相同的字符,那么我们转移的时候可以考虑分这个区间首尾的字符是不是相同的,假如是相同的,那么 dp[i][j]=min(dp[i+1][j],dp[i][j-1]),因为区间最后两端的字符都是可以最后进行操作的,这并不会影响,我们可以理解为顺便把端点的糖果给吃了,所以转移的时候就不会增加权值。假如不是相同的,那么就只能分为两个区间来吃掉这个区间的糖果,然后枚举中间值,dp[i][j]=min(dp[i][j],dp[i][k],dp[k+1][j])

但是这样会发现时间复杂度是 ,计算量似乎是 10 亿,但是这里需要注意,第三层循环只会枚举区间长度,然后最坏的情况计算量不会超过 2 亿,在常数很小的 dp 中足够。

AC_Code

  • C/C++
//
// Created by liuhao.
//
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
#define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define vii vector<vector<int>>

signed main()
{
    ios;
    int t;
    cin>>t;
    while(t--)
    {
        int n,c;
        cin>>n>>c;
        string s;
        cin>>s;
        s=" "+s;
        vii dp(n+1,vector<int>(n+1,0x3f3f3f3f));
        for (int i = 1; i <= n; i++) {
            dp[i][i]=1;
        }
        for(int len=2;len<=n;len++)
        {
            for(int i=1;len+i-1<=n;i++)
            {
                int j=len+i-1;
                if(s[i]==s[j])dp[i][j]=min(dp[i+1][j],dp[i][j-1]);
                else
                {
                    for(int k=i;k<j;k++)
                    {
                        dp[i][j]=min(dp[i][k]+dp[k+1][j],dp[i][j]);
                    }
                }
            }
        }
        cout<<(dp[1][n]<=c?"YES":"NO")<<endl;
    }
    return 0;
}

C. FF 爱吃糖

解题思路

1、只要考虑每颗糖果的 happy 值模上 的结果。

2、对于 happy 值为 的糖果,模上 的结果为 ,证明如下:

3、思考拿的糖果满足什么条件才能使得剩余糖果的总价值模上 的结果不变:拿的糖果的总价值是 的倍数。

4、根据上面三点,我们可以得知答案就是:从 颗糖果中选择若干个,总 happy 值为 的倍数的方案数。

5、总 happy 值为 的倍数等价于:总 happy

6、我们考虑 :定义 表示从前 颗糖中选,总 值模上 的结果为 的方案数。

根据第 颗糖果选与不选对集合进行划分:

不选第 颗糖果的方案数为

选第 颗糖果的方案数为

所以

7、答案为

AC_Code

  • C/C++
#include<bits/stdc++.h>

using namespace std;

const int N = 5010, mod = 1e9 + 7;

int f[N][N];
int a[N];
int n,p;
string s;

signed main()
{
    cin >> n >> p;
    for(int i=1; i<=n; i++)
    {
        cin >> a[i] >> s;
    }
    f[0][0] = 1;
    for(int i=1; i<=n; i++)
    {
        for(int j=0; j<p-1; j++)
        {
            f[i][j] = f[i-1][j] + f[i-1][((j-a[i]) % (p-1) + (p-1))%(p-1)];
            f[i][j] %= mod;
        }
    }
    cout << f[n][0];
}

D. FF 分糖果

题解

解题思路,我们可以将小朋友的编号和要求视为有向图。

小朋友代表节点,小朋友的要求代表有向边,小朋友 想比小朋友 个糖果相当于添加一条有向边:从 的边,权重为

根据题目要求,我们只需要了解各个小朋友之间的糖果关系即可,可以根据上面的建图思想,用带权的并查集解决。

我们可以认定答案初始值为总要求 ,遇到不合法的要求,即不合法的关系,答案就减一,最后输出答案。

AC_Code

  • C/C++
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int d[N],p[N];
int find(int x)
{
    if(x!=p[x])
    {
        int root=find(p[x]);
        d[x]+=d[p[x]];
        p[x]=root;
    }
    return p[x];
}
void solve()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        p[i]=i;
        d[i]=0;
    }
    int ans=m;
    while(m--)
    {
        int a,b,dis;
        cin>>a>>b>>dis;
        int x=find(a),y=find(b);
        if(x!=y)
        {
            d[x]=dis-d[a]+d[b];
            p[x]=y;
        }
        else{
            if(dis!=d[a]-d[b]) ans--;
        }
    }
   cout<<ans<<'\n';
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr),cout.tie(nullptr);
    int t;
    cin>>t;
    while(t--)
    {
        solve();
    }
    return 0;
}

E. 美味的果实

解题思路

先判断第一次结果是否在假期后,是的话假期内结果天数为 ,否则计算剩余天数除结果周期即为剩余假期结果天数,再加上第一天即为答案。

AC_Code

  • C/C++
#include<iostream>
using namespace std;
int main()
{
 int n,m,p;
 cin>>n>>m>>p;
 if(m>n)cout<<0;
 else cout<<1+(n-m)/p;
 return 0;
}

F. 贪玩的 FF

解题思路

​ 求一个区间中两两消除后个数不超过一个,即数量为奇数个的数不超过 个。由于,考虑将每一个数变成 ,于是我们可以得到一个新的序列 ,于是区间 满足 Link-Delete 的条件就变成了 ^ ^ ^ 的整数次幂。

​ 考虑维护一个前缀异或数组 ,于是问题就变成了求区间 中有多少对 满足 ^ 的整数次幂。由于此题为没有修改操作,使用离线算法莫队维护,提前将序列中的每一个数对应的答案预处理出来最多为 个,莫队每次修改的时候统计答案, 时间复杂度

AC_Code

  • C/C++
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 1e5 + 10;
unordered_map<int,int> mp; //离散化
int cnt[N];
int blk[N],s[N];
vector<vector<int>> f;
int n,m;
struct node{
    int l,r,id;
    bool operator <(const node&a) const{
        if(blk[this->l] == blk[a.l])return this->r < a.r;
        return blk[this->l] < blk[a.l];
    }
}ask[N]; 

void upt(int p,int v,i64 &res){ //更新操作,每次修改的时候查询区间中满足条件的个数
    if(v == 1){
        for(auto x : f[p]){
            res += cnt[x];
        }
        cnt[mp[s[p]]] ++;
    }
	else{
        cnt[mp[s[p]]] --;
        for(auto x : f[p]){
            res -= cnt[x];
        }
    }
}
int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin >> n >> m;
    int tt = 1;mp[0] = tt++;
    for(int i = 1; i <= n; i++){
        int x;
        cin >> x;
        s[i] = 1<<(x-1),s[i] ^= s[i-1];
        if(!mp.count(s[i]))mp[s[i]] = tt++;
    }
    f.resize(n+1);
    for(int i = 0; i <= n; i++){
        f[i].push_back(mp[s[i]]);
        for(int j = 1; j < 31; j++){ //预处理所有满足条件的数
            int x = s[i] ^ (1<<(j-1));
            if(mp.count(x))f[i].push_back(mp[x]);
        }
    }
    
    int len = sqrt(n),t = 1; //分块
    for(int i = 1; i <= n; i++){
        if(i > t*len)t++;
        blk[i] = t;
    }

    for(int i = 1; i <= m; i++){
        int l,r;
        cin >> l >> r;
        ask[i] = {l-1,r,i};
    }
    sort(ask+1,ask+1+m); //对询问进行排序

    i64 res = 0;
    int l = 0,r = -1;
    vector<i64> ans(m+1);
    for(int i = 1; i <= m; i++){ //莫队操作 模板
        while(r > ask[i].r)upt(r--,-1,res);
        while(r < ask[i].r)upt(++r,1,res);
        while(l < ask[i].l)upt(l++,-1,res);
        while(l > ask[i].l)upt(--l,1,res);

        ans[ask[i].id] = res;
    }
    for(int i = 1; i <= m; i++)cout << ans[i] << "\n"; //输出答案
}

G. 小杰同学去旅游

解题思路

每次运输只有一个人递送校园卡即可。

当校园卡数目大于等于人数的时,所有人一次就进入校门,可以直接输出为

当校园卡数目小于或者等于一的时,只有一个人能进入,或者所有人都不能进入,直接输出为

其他情况,用 来记录一共几个来回,每一次只能帮助 个人进入校园,用 记录一共有多少个来回,到最后,如果 时,还有一小部分人没有进入校园, 再加一,最后计算所需要的时间, 为来回需要的次数, 表示加上第一次送入的所需要的时间。

AC_Code

#include<bits/stdc++.h>
#include <cmath>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")

#define endl '\n'

#define pii pair<int,int>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);

const int maxn=1e6+5;

int main(){
    int t;
    cin>>t;

    while(t--){
        ll n,m,ans=0;
        cin>>n>>m;

        if(m>=n)cout<<1<<endl;
        else if(m==1)cout<<-1<<endl;
        else {
            n-=m;
            ans++;
            ans+=n/(m-1)*2;
            if(n%(m-1))ans+=2;
            cout<<ans<<endl;
        }
    }
}

H. 偶遇

解题思路

本题给出一张无向连通图,我们首先要将这张图上的环全部缩为一个点,无向图的连通分量我们可以想到边双和点双,点双会将两个不构成环的点缩在一起,所以我们考虑边双。但是其实我们可以把无向图当成有向图搜,然后使用强连通分量版的 算法。

在缩完点之后,能否偶遇其实就是判断这两段路径是否相交,两段路径是否可以相交我们通过多画几张图就能看出,肯定存在其中一条路径的两个端点的最近公共祖先在另一条路径上,而判断一个点是否在一条路径上,我们可以看这个点到两端的距离加起来是否等于路径长度。而在树上我们可以通过 快速算出两点间的距离。

时间复杂度为

AC_Code

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;

const int N=1e5+10,M=1e6+10;

int hs[N],h[N],e[M],ne[M],idx;

void add(int h[],int a,int b){
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

int n,m,q;
int dfn[N],low[N],timestamp;
int stk[N],top;
bool in_stk[N];
int scc_cnt,id[N];
int depth[N],fa[N][18];

void bfs(){
    queue<int>q;
    q.push(1);
    memset(depth,0x3f,sizeof(depth));
    depth[0]=0,depth[1]=1;
    while(q.size()){
        int t=q.front();
        q.pop();
        for(int i=hs[t];~i;i=ne[i]){
            int j=e[i];
            if(depth[j]>depth[t]+1){
                depth[j]=depth[t]+1;
                q.push(j);
                fa[j][0]=t;
                for(int k=1;k<=17;k++){
                    fa[j][k]=fa[fa[j][k-1]][k-1];
                }
            }
        }
    }
}

int lca(int a,int b){
    if(depth[a]<depth[b])swap(a,b);
    for(int k=17;k>=0;k--){
        int t=fa[a][k];
        if(depth[t]>=depth[b]){
            a=t;
        }
    }
    if(a==b)return a;

    for(int k=17;k>=0;k--){
        if(fa[a][k]!=fa[b][k]){
            a=fa[a][k],b=fa[b][k];
        }
    }
    return fa[a][0];
}

void tarjan(int u,int f){
    dfn[u]=low[u]=++timestamp;
    stk[++top]=u,in_stk[u]=true;

    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(j==f)continue;
        if(!dfn[j]){
            tarjan(j,u);
            low[u]=min(low[u],low[j]);
        }
        else if(in_stk[j])low[u]=min(low[u],dfn[j]);
    }

    if(dfn[u]==low[u]){
        int y;
        scc_cnt++;
        do{
            y=stk[top--];
            in_stk[y]=false;
            id[y]=scc_cnt;
        }while(y!=u);
    }

}

int distance(int a,int b){
    return depth[a]+depth[b]-2*depth[lca(a,b)];
}

int main(){
    scanf("%d%d%d",&n,&m,&q);

    memset(h,-1,sizeof(h));

    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        add(h,a,b),add(h,b,a);
    }

    for(int i=1;i<=n;i++){
        if(!dfn[i])tarjan(i,0);
    }

    memset(hs,-1,sizeof(hs));
    for(int u=1;u<=n;u++){
        for(int i=h[u];~i;i=ne[i]){
            int j=e[i];
            in***],b=id[j];
            if(a!=b){
                add(hs,a,b);
            }
        }
    }

    bfs();



    while(q--){
        int a,b,c,d;
        scanf("%d%d%d%d",&a,&b,&c,&d);
        int aa=id[a],bb=id[b],cc=id[c],dd=id[d];
        int lca1=lca(aa,bb),lca2=lca(cc,dd);
        if(distance(aa,bb)==distance(aa,lca2)+ distance(lca2,bb)){
            printf("Yes\n");
            continue;
        }
        if(distance(cc,dd)== distance(cc,lca1)+ distance(lca1,dd)){
            printf("Yes\n");
            continue;
        }
        printf("No\n");
    }
    return 0;
}

I. 序列·终

解题思路

由等差数列的求和公式:

我们第一个数为,第二个数为

则我们可以得到

所以我们可以通过枚举 的值的方式来计算个数,时间复杂度

AC_Code

  • C++
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
#include<set>
#include<map>
#include<cmath>
using namespace std;
#define int long long 
#define endl "\n"
const int N = 3e5+10;
int n,m,k;
void solve()
{
    cin>>n;
    int res=0;
    n*=2;
    for(int i = 1;i*i<=n;i ++ )//枚举(l+r)的所有情况。
    {
        if(n%i==0&&(i%2)!=(n/i)%2)res++;//判断是否存在(l+r)与(r-l+1),必须同奇或者同偶才能成立。
    }
    cout<<res*2<<endl;//有负数存在的情况所以要乘2。
}
signed  main()
{
    ios_base::sync_with_stdio(0); cin.tie(0);cout.tie(0);
   int t=1;
   
   while(t--)
   {
       solve();
   }
    return 0;
}