Fork me on GitHub

2025.8.13校队题单分享+总结

那个随笔写不下了,先暂时放这

T1


由于电信技术的发展,人人都可以通过手机互相联系。

有一位电信大佬最近想生产一大批手机,然而从生产线上一台一台地生产实在太慢了,于是他想出了一个办法 —— 让手机自我复制。
于是他给手机加上了一个内置的函数 fork()。手机的程序如果调用这个函数,那么手机会生产出一台完全一模一样的手机(包括程序运行状态),并且自己这台的函数返回值为 \(1\) ,新手机的函数返回值为 \(0\) ,然后两台手机都继续执行程序。(请注意黑体字内容)

初始时,只有一台手机。接着,大佬让手机计算形如这样的表达式:

fork() <op> fork() <op> ... <op> fork()

其中 是二元运算符,为 && 或者 || 中的一种。例如:

fork() && fork() || fork() && fork() && fork() || fork()

两个运算都是左结合的,且 && 的优先级比 || 高,所以上面的那个表达式相当于:

((fork() && fork()) || ((fork() && fork()) && fork())) || fork()

对于表达式 \(a\) && \(b\),手机会先计算 a 的值,如果为 \(0\) 那么不计算 \(b\) 的值(因为很重要所以说两遍,请注意这里不计算 \(b\) 的值),该表达式值为 \(0\);否则计算 \(b\) 的值并将其值作为该表达式的值。

对于表达式 \(a\) || \(b\),手机会先计算 \(a\) 的值,如果为 \(1\) 那么不计算 \(b\) 的值(因为很重要所以说两遍,请注意这里不计算 \(b\) 的值),该表达式值为 \(1\);否则计算 \(b\) 的值并将其值作为该表达式的值。

表达式计算完成后,大佬制造出了数量惊人的手机,人类终于叩开了指数级工业制造的大门。

一万万年后,一位考古学家调查了此次事件。他得到了大佬让手机计算的表达式。他想知道大佬当年究竟制造出了多少台手机。(包括初始的那台手机)

你可以参照样例解释来更好地理解题意。

输入格式
第一行一个正整数 \(n\),表示表达式中的 \(fork()\) 的数量。

接下来一行 \(n−1\) 个用空格隔开的字符串,每个字符串为 "&&” 或者 “||”,依次表示表达式中对应位置的运算符。

输出格式
一行,一个整数表示制造出的手机的数量,你只用输出答案对 \(998244353\) 取模后的结果。

样例一

input

2
&&

output

3

explanation

共生产 \(3\) 台手机,过程如下:

\(1\) 台手机开始计算 \(fork()\) && \(fork()\)
\(1\) 台手机开始计算 \(fork()\),产生了第 \(2\) 台手机。
\(1\) 台和第 \(2\) 台的 \(fork()\) 计算完成,第 \(1\) 台返回 \(1\),第 \(2\) 台返回 \(0\)
\(1\) 台手机由于 \(fork()\) 返回值为 \(1\),开始计算 \(fork()\) && \(fork()\) 右边的 \(fork()\),产生了第 \(3\) 台手机。
\(2\) 台手机由于 \(fork()\) 返回值为 \(0\) ,于是 \(fork()\) && \(fork()\) 值为 \(0\)(跳过右边的 \(fork\) 的计算),程序结束。
\(1\) 台和第 \(3\) 台的 \(fork()\) 计算完成,第 \(1\) 台返回 \(1\),第 \(3\) 台返回 \(0\)
\(1\) 台手机由于 \(fork()\) 返回值为 \(1\),于是 \(fork()\) && \(fork()\) 值为 \(1\),程序结束。
\(3\) 台手机由于 \(fork()\) 返回值为 \(0\),于是 \(fork()\) && \(fork()\) 值为 \(0\),程序结束。

样例二
input

6
&& || && && ||

output

15

限制与约定: \(n \le 10^5\)


题解

因为 && 的优先级高于 || 就将 || 作为分割线,每一块 && 依次处理。
观察题目,如果是 && 那么前面是 \(0\) 就停止,如果是 || 那么前面是 1 就停止
只有当手机返回值为1时才能造出手机。而且在当前这块中复制出的手机,在下一块中才能造出其他的手机。
所以就引导出了答案等于块长的前缀乘加在一起。

接下来就是最简单的实现了:


#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 1e5 + 10;
const int mod = 998244353;

int n;
string str;
int cont[N], cnt = 0, idx = 0;

signed main()
{
    cin >> n;
    cnt = 1;
    for(int i = 1 ; i < n ; i ++ )
    {
        cin >> str;
        if(str[0] == '|') cont[ ++ idx] = cnt, cnt = 1;
        else cnt ++ ;
    }
    cont[ ++ idx] = cnt;
    int now = 1, ans = 1;
    for(int i = 1 ; i <= idx ; i ++ )
    {
        now = (now * cont[i]) % mod;
        ans = (ans + now) % mod;
    }
    cout << ans;
    return 0;
}

T2


关羽喜欢下象棋!

不过这次,他下腻了传统象棋,并叫来了你做他的对手。你们将在一张 \(100\)\(100\) 列的象棋棋盘格点上对弈。关羽一身傲骨,给你了一辆大幅加强的车,自己则操纵一个小过河卒东躲西藏。具体规则如下:

  1. 卒初始在第 \(x_1\) 行第 \(y_1\) 列的格点上,车初始在第 \(x_2\) 行第 \(y_2\) 列的格点上。

  2. 卒每次可以在左、右、下三种移动方向中选择一种,然后移动一格(但是不能往上)。即,若记第 \(x\) 行第 \(y\) 列的格点为 \((x,y)\),则卒可以从 \((x,y)\) 移动到 \((x+1,y),(x,y+1),(x,y−1)\)

  3. 车可以向左向右移动多格,也可以向上向下移动多格,也可以不动。即,车可以从 \(x,y\) 移动到 \((x,y′)(x,y′) 或(x′,y)(x′,y)\),其中 \(1 \le x′,y′ \le 100\)

  4. 卒和车均不可以走到棋盘外。

  5. 这辆车经过现代科技改造,会沿路散发毒气,车经过的格点都会被毒雾覆盖,卒不能停留。例如,如果车从 \((x,y)\) 向右移动到 \((x,y′)(y′>y)\),则 \((x,y),(x,y+1),…,(x,y′)\) 都会带毒。其余三种移动方向类似。

  6. 这辆车不可被摧毁,即卒不能吃车,也不能移动到车占据的位置。

b913a8ae2d7da20bbb9005066404a0fe

聪明的你发现你可以因此吊打武神关羽!于是你非常好奇,你最快几步可以击败关羽。这个特殊的象棋分为若干回合,每回合是这样进行的:

  1. 你操控车移动一次,也可以选择不动。
  2. 如果车吃掉了卒(即车占据了卒所在位置),游戏结束。
  3. 卒移动一步。当且仅当卒没有可移动方向时,卒才可以选择不动(即左、右、下三个方向均为车、毒气、棋盘边界中的一种)。例如,如果进行到某一轮前,\((1,2)\) 有毒雾,且该回合车从 \((2,2)\) 移动到 \((2,1)\),那么卒无可移动方向,故卒该轮不进行移动。

d9154141b645a860a0f53b3a0d4c4d8a

游戏总回合数定义为车决策的次数。

当然武神也很聪明,他希望游戏回合数尽可能多,而你希望游戏回合数尽可能少,并且你们都足够聪明。你想提前知道,游戏将会进行几回合?

样例一
input

4
1 1 2 2
1 2 2 4
100 50 3 3
50 2 49 4

output

2
3
2
3

explanation

对于第一组数据,车可以选择停在原地,而轮到卒的时候卒必须移动。无论向下还是向右,都会立马被车吃掉。

448ccc346d97c7a69bf821df5262b69b

注意这里只画了棋盘左上角。

对于第二组数据,车可以先在卒下方洒出一行毒雾,然后再走到卒所在的第一行,即可必杀。

415f69d3261cae3257ab584e45d884ff

对于第三组数据,车移动到 \((100,3)\),即可下一轮必杀。

对于第四组数据,答案是 \(3\),这是为什么呢?

1065882f70fb27054ac70cf83fe6ee69

一道很有意思的题,我们要先控制一下,答案的最大值,我们可以考虑怎样关羽必输?观察到关羽不能往后走,所以只要把关羽的前一行弄满
毒气,这样就把关羽锁在了一行里,最后直接走到关羽的那一行就好了,那最少要多少步呢?

image

事实证明,最多只要四步就能完成,接下来就只要爆搜亦或者分类讨论就好了,我写的是分类讨论 :


#include <bits/stdc++.h>

#define int long long

using namespace std;


int T;

signed main()
{
    cin >> T;
    while(T -- )
    {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        if(a == c || b == d) cout << 1 << endl;
        else 
        {
            if(b > d) b = 100 - b + 1, d = 100 - d + 1;
            if(a == 100) cout << 2 << endl;
            else if(b == 1)
            {
                if(c == a + 1 || d == 2) cout << 2 << endl;
                else cout << 3 << endl;
            }else if(b == 2)
            {
                if(c == a + 1 || d == 3) cout << 3 << endl;
                else if(c <= a && d == 4) cout << 3 << endl;
                else{
                    if(c == a + 1) cout << 3 << endl;
                    else if(a == 99) cout << 3 << endl;
                    else cout << 4 << endl;
                }
            }else{
                if(c == a + 1) cout << 3 << endl;
                else if(a == 99) cout << 3 << endl;
                else cout << 4 << endl;
            }
        }
    }
    return 0;
}

T3

新年的毒瘤


辞旧迎新之际,喜羊羊正在打理羊村的绿化带,然后他发现了一棵长着毒瘤的树。

这个长着毒瘤的树可以用 \(n\) 个结点 \(m\) 条无向边的无向图表示。这个图中有一些结点被称作是毒瘤结点,即删掉这个结点和与之相邻的边之后,这个图会变为一棵树。树也即无简单环的无向连通图。

现在给你这个无向图,喜羊羊请你帮他求出所有毒瘤结点。

输入格式
第一行两个正整数 \(n\), \(m\),表示有 \(n\) 个点 \(m\) 条边。保证 \(2 \le n\)

接下来 \(m\) 行,每行两个整数 \(v,u\),表示 \(v\)\(u\) 之间有一条无向边。\(1 \le v,u \le n\)。保证没有重边和自环。

input

6 6
1 2
1 3
2 4
2 5
4 6
5 6

output

3
4 5 6

首先呢,是一棵树,那么得联通,所以呢我们删掉的这个点一定不能是割点, 这直接用点双来求割点就好了,其次呢,就是不能有环,也就是删完之后得剩 \(n-2\) 条边,就直接判断就好了。很简单,给一个丑陋的代码:

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 1e5 + 10;
const int M = 2e5 + 10;

int n, m;
vector<int> edge[N];
int dfn[N], low[N], timestamp;
bool st[N];

void tarjan(int u, int fa)
{
    dfn[u] = low[u] = ++ timestamp;
    int son_cnt = 0;
	for (int i = 0 ; i < edge[u].size() ; i ++ ) 
	{
		int j = edge[u][i];
        if (!dfn[j])
        {
            son_cnt ++ ;
            tarjan(j, u);
            low[u] = min(low[u], low[j]);
            if(low[j] >= dfn[u]) st[u] = true;
        }else if (dfn[j] < dfn[u] && j != fa) low[u] = min(low[u], dfn[j]);
    }
	if (fa < 0 && son_cnt == 1) st[u] = 0;
}

signed main()
{
    cin >> n >> m;
	for(int i = 0 ; i < m ; i ++ )
	{
	    int a, b;
	    cin >> a >> b;
	    a -- ;
	    b -- ;
		edge[a].push_back(b);
		edge[b].push_back(a);
	}
	tarjan(0, -1);
	vector<int> ans;
	for(int i = 0 ; i < n ; i ++ )
		if(st[i] == false && m - edge[i].size() == n - 2)
			ans.push_back(i + 1);
    sort(ans.begin(), ans.end());
    cout << ans.size() << endl;
    for(int i = 0 ; i < ans.size() ; i ++ ) cout << ans[i] << " ";
    return 0;
}

T4

票数统计

妹滋滋是一个善于编程的女孩子。

但是某一天,她一不小心把 UOJ 后台的票数统计程序写错了。

本来嘛在这种根本没有什么用的功能上出了 bug 也没有什么大关系,但是又有某一天,UOJ 突然就开始搞全民公投了。

这可怎么办呢?如果这个消息让别人知道的话自己肯定会被查表,更不要说让所有用户重新来投一次票了。

作为一个要强的女孩子,妹滋滋决定自力更生。

通过一些奥妙重重的方式,妹滋滋知道了一些关于这次全民公投的信息。

  1. 这次全民公投一共有 \(n\) 位用户排队参加,编号为 \(1\)\(n\)。每一位用户要么投了通过,要么投了不通过。
  2. \(m\) 个二元组 \((x_i,y_i)\),每个二元组给出这样一个信息: “前 \(x_i\) 位用户中,恰好 \(y_i\) 位投了通过” 和 “后 \(y_i\) 位用户中,恰好有 \(x_i\) 位投了通过” 这两句话中,至少有一句是成立的。

作为分析的第一步,她想要知道有多少种投票情况是满足她所得到的信息的。当然,可能所有投票情况都不满足条件。

解法:考虑组合数

\(x>y\) 时条件为前缀限制,\(x<y\) 时条件为后缀限制。
既有前缀限制,又有后缀限制的情况下,我们枚举总共1的个数,把后缀限制转化为前缀限制。
如果所有限制均有 \(x \not= y\) 则可以直接使用组合数计算。预处理组合数,单次计算的时间复杂度是 \(O(n)\) 的。

当有 \(x=y\) 时,显然只需要考虑所有 \(x = y\) 限制中 \(x\) 最大的限制即可,总方案数为满足前缀+满足后缀-满足前缀和后缀。时间复杂度 \(O(n^2)\)

代码就不给了

T5

T5很简单,就不总结了,最然当时其他同学没有分享的时候还不会,但是是脑抽没想到,真的没啥好总结的

posted @ 2025-08-13 10:01  tony0530  阅读(22)  评论(0)    收藏  举报