集美大学第七届团体程序设计天梯赛第一场排位赛题解

L1

L1-1 I Say TingTing

题目大意

婷婷是谁!

解题思路

正确答案为D以下三种解题方法

  1. 知道天梯赛总决赛于423号比赛
  2. 知道婷婷是何大佬的女朋友
  3. 枚举大法

代码

#include<stdio.h>
int main(){
    printf("D");
}

L1-2魂环极限

出题人 : 赖

题目大意:

根据大小判断应该输出什么。

  • 测试点详情 :全都是随机random随机1~1e18的数字

解题思路

	if 小于 输出答案。
    else if
    ......

代码实现

#include <bits/stdc++.h>
#define MAXN 100010
using namespace std;
long long a;
int main() {
  cin >> a;
  while (a--) {
    long long in;
    cin >> in;
    if (in <= 423)
      cout << 10 << endl;
    else if (in <= 764)
      cout << 20 << endl;
    else if (in <= 1760)
      cout << 30 << endl;
    else if (in <= 5000)
      cout << 40 << endl;
    else if (in <= 12000)
      cout << 50 << endl;
    else if (in <= 20000)
      cout << 60 << endl;
    else if (in <= 50000)
      cout << 70 << endl;
    else if (in <= 99999)
      cout << 80 << endl;
    else
      cout << 90 << endl;
  }
}

L1-3汉字序顺

出题人 : 赖

题目大意

对比上下单词是否不一样,并且统计数量。

解题思路

因为单词数量相等,直接创建两个字符串数组,然后依次对比过去遇到不同就ans++最后输出ans就可以了。

代码实现

#include <bits/stdc++.h>
using namespace std;
string s1, s2;
vector<string> split(string str) {
  int size = str.size();
  vector<string> ret;
  int jl = 0;
  for (int i = 0; i < size; i++) {
    if (str[i] == ' ') {
      ret.push_back(str.substr(jl, i - jl));
      jl = i;
    }
  }
  ret.push_back(str.substr(jl, size - jl));
  return ret;
}
int main() {
  int ans = 0;
  getline(cin, s1);
  getline(cin, s2);
  vector<string> ss1 = split(s1);
  vector<string> ss2 = split(s2);
  for (int i = 0; i < ss1.size(); i++) {
    if (ss1[i] != ss2[i]) {
      ans++;
    }
  }
  printf("%d", ans);
}

L1-4晦气的原批

出题人 : 赖

题目大意

顺序判断字符串,根据字符串出现的顺序,统计一个值,这里注意的一点是初始是0。

解题思路

​ 依旧是依次读入判断字符串是否与op、ys、yuanshen、genshin一样,如果一样就对ans进行对应的操作,操作结束之后对1e9取余。

代码实现

#include <bits/stdc++.h>
using namespace std;
string ss;
long long ans = 0;
int main() {
  while (cin >> ss) {
    if (ss == "op") ans++;
    if (ss == "ys") ans *= 2;
    if (ss == "yuanshen") ans *= 5;
    if (ss == "genshin") ans *= 10;
    ans %= 1000000007;
  }
  cout << ans;
}

L1-5疑心病的老板

出题人 :杰

题意

给定 n , k n,k n,k 。一个 [ 1 , 2 , … … , k − 1 , k , k − 1 , … … , 2 , 1 , 0 , 0 , 0 … … , 0 ] [1,2,……,k-1,k,k-1,……,2,1,0,0,0……,0] [1,2,……,k1,k,k1,……,2,1,0,0,0……,0] 。求前 n n n 项和。

解题思路

分三种情况求等差数列前 i i i 项和即可。

f ( l , r ) f(l,r) f(l,r) [ l , l + 1 , … … , r − 1 , r ] [l,l+1,……,r-1,r] [l,l+1,……,r1,r] 的和。

n < = k n<=k n<=k,答案为: f ( 1 , n ) f(1,n) f(1,n)

n > k & & n < = 2 × k − 1 n>k\&\&n<=2\times k-1 n>k&&n<=2×k1,答案为: f ( 1 , k ) + f ( 2 k − n , k − 1 ) f(1,k)+f(2k-n,k-1) f(1,k)+f(2kn,k1)

n > 2 × k − 1 n>2\times k-1 n>2×k1,答案为: f ( 1 , k ) + f ( 1 , k − 1 ) f(1,k)+f(1,k-1) f(1,k)+f(1,k1)

代码

#include<bits/stdc++.h>
using namespace std;

long long f(long long l,long long r){
	return (l+r)*(r-l+1)/2;
}
int main() {
	long long n,k;
	cin>>n>>k;
	if(n<=k)cout<<f(1,n);
	else if(n<=2*k-1)cout<<f(1,k)+f(2*k-n,k-1);
	else cout<<f(1,k)+f(1,k-1);
	return 0;
}

L1-6有色图

出题人:贝

题目大意

在二维数组出现不同的数字的数量是否超过 9 9 9

出题报告

  • 测试点1:小数据 n , m n,m n,m
  • 测试点2: n = a i = b i = 100 n=a_i=b_i=100 n=ai=bi=100 x i j x_{ij} xij的范围在 i n t int int之内
  • 测试点3: n = a i = b i = 100 n=a_i=b_i=100 n=ai=bi=100 x i j x_{ij} xij的范围在 l o n g   l o n g long\ long long long之内
  • 测试点3: n = a i = b i = 100 n=a_i=b_i=100 n=ai=bi=100 x i j x_{ij} xij的范围超过 l o n g   l o n g long\ long long long,需要用map<string, int来统计

解题思路

使用map<string, int来统计出现的数的数量即可,最后判断map.size() > 9是否成立

代码

#include <bits/stdc++.h>
using namespace std;
int T, a, b;
int main()
{
    cin >> T;
    while (T--)
    {
        map<string, int> mp;
        cin >> a >> b;
        string k;
        for (int i = 1; i <= a; ++ i)
        {
            for (int j = 1; j <= b; ++ j)
            {
                cin >> k;
                ++ mp[k];
            }
        }
        if(mp.size() > 9)
            cout << "YES";
        else 
            cout << "NO" ;
        if(T)
            cout << '\n';
    }
}

L1-7成绩排名

出题人 : 弛

题目大意

  • 给出每个人的三门课成绩,然后按照给出的规则进行排序

出题报告

  • 第一个测试点三个人的情况,包含同总分,同第一门课,同第二门课,同第三门课成绩的情况
  • 第二个测试点一个人或两个人的情况
  • 第二个测试点一个人或两个人的情况
  • 第三,四个测试点大量随机数据

解题思路

  • 主要考点是结构体排序,自定义排序规则

  • 给出的排序限制不能交换相邻的两个,这个限制只对人数小于等于3的时候有影响

  • 如果只有一个人,直接输出就可

  • 如果只有两个人,两个人无法交换,直接输出就可

  • 如果只有三个人,第二的人的位置无法变动,对第一第二个人进行排序,保持第二个人不动即可

  • 如果是四个人即以上可以证明这个限制没有影响

    • 原本排序 abcd->adcb->bdca->bacd实现了前两个人的交换

    • 原本排序 abcd->adcb->bdca->cdba->adbc->acbd 实现了中间两个人的交换

代码实现

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x3f3f3f3f
struct node{
    int a,b,c;
    int all,index;
    string s;
}a[200000+10];
bool tmp(node a,node b){
    if(a.all==b.all){
        if(a.a==b.a){
            if(a.b==b.b){
                if(a.c==b.c){
                    return a.index<b.index;
                }
                return a.c>b.c;
            }
            return a.b>b.b;
        }
        return a.a>b.a;
    }
    else return a.all>b.all;
}
char ch[100];
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%s%d%d%d",&ch,&a[i].a,&a[i].b,&a[i].c);
            a[i].s=ch;
            a[i].all=a[i].a+a[i].b+a[i].c;
            a[i].index=i;
        }
        if(n==2){
            printf("%s %s \n",a[1].s.c_str(),a[2].s.c_str());
        }
        else if(n==3){
            a[2].all=500;
            sort(a+1,a+1+n,tmp);
            printf("%s %s %s \n",a[2].s.c_str(),a[1].s.c_str(),a[3].s.c_str());
        }
        else{
            sort(a+1,a+1+n,tmp);
            for(int i=1;i<=n;i++){
                printf("%s ",a[i].s.c_str());
            }
            printf("\n");
        }
    }
}

L1-8字符串拆分

出题人 : 举

题意

把字符串拆分成两个或以上的连续子字符串,使其数值递减,并且相邻的数值差为1

解题思路

这题只要暴力枚举每个子串,并且加上特判剪枝就行。

对于一个字符串,假设从下标为0开始到下标i为它的第一个字串,那对剩下的部分的字符串,也可以重复之前的操作,继续把前i个进行分割。当前一个数值刚好是当前数值+1,就可以枚举下一个字符串了。并且题目保证分割后的数值不会超过long long,所以其实当子字符串数值大于1e11的时候,就可以停止了。

代码

#include<bits/stdc++.h>
#include <random>
#include <chrono>
using namespace std;

typedef long long ll;

class Solution {
public:
    int len;

    bool dfs(string& s, ll pre, ll cur, int idx) {
        if (idx == len) return cur + 1 == pre && pre != -1;

        bool flag = false;

        cur = cur * 10 + (s[idx] - '0');
        if (cur > 1e11)return false;

        if (pre == -1 || cur + 1 == pre) {
            if (idx < len - 1) flag = flag || dfs(s, cur, 0, idx + 1);
        }
        flag = flag || dfs(s, pre, cur, idx + 1);

        return flag;
    }

    bool splitString(string s) {
        len = s.size();
        return  dfs(s, -1, 0, 0);
    }
};
int main() {
    ios::sync_with_stdio(false);
    int n;
    cin >> n;
    Solution s;
    while (n--) {
        string str;
        cin >> str;
        cout << ((s.splitString(str)) ? "yes\n" : "no\n");
    }
	return 0;
}

L2

L2-1计算中缀表达式

出题人 : 弛

题目大意

  • 给出中缀表达式计算结果,运算符包含+,-,*,/,(,)

出题报告

  • 第一个测试点只有加减和负号
  • 第二个测试点有加减乘除和负号
  • 第三个测试点有加减乘除和括号和负号,带大量括号
  • 第四个测试点加减乘除和多重括号和负号,带大量括号
  • 第五、六两个测试点随机数据,带大量括号

特别的对于后四个点,因为带了大量的括号,导致使用python的eval计算出现暴栈的问题,所以该题用python中的eval函数只能拿到两个测试点的分数,如果使用python语言还需要手写模拟栈,且本题给出的时间足以让python模拟

解题思路

  • 先定于优先级 (最低,然后是+,-,然后是*,/

  • 模拟堆栈,用运算符的优先级控制出入栈

  • 如果是数字直接进入数字栈

  • 如果输入的 - ,先判断这个是负号还是减号,如果是算式的第一个或者这个符号的前面不是数子就是负号

  • 如果是负号的号,读入这个负数入数字栈

  • 如果是+,-,*,/运算符的话判断运算符堆栈中的情况

    • 栈空直接入栈

    • 否则一直出栈其中的内容,并进行相应的运算,直到栈顶的优先级小于当前这个符号的优先级,然后入栈当前符号

  • 如果是 ( 的情况直接入栈

  • 如果是 )的情况直接入栈,一直出栈符号栈,进行相应的运算,知道栈顶是(的时候,出栈(这个符号

  • 最后把栈中所有符号逐个出栈计算出最终结果

代码实现

#include <bits/stdc++.h>
using namespace std;
stack<double> a;
stack<char> b;
int getyou(char ch)
{
    if (ch == '(')
        return 1;
    else if (ch == '+' || ch == '-')
        return 2;
    else if (ch == '*' || ch == '/')
        return 3;
    else
        return 4;
}
int main()
{
    char ch[1000];
    string s;
    cin >> s;
    int len = s.length();
    int index = 0;
    while (index < len)
    {
        if (s[index] >= '0' && s[index] <= '9')
        {
            double num = s[index] - '0';
            index++;
            while (s[index] >= '0' && s[index] <= '9' && index < len)
            {
                num = num * 10 + s[index] - '0';
                index++;
            }
            a.push(num);
        }
        else if (s[index] != '(' && s[index] != ')')
        {
            if (s[index] == '-' && (index == 0 || getyou(s[index - 1]) <= 3))
            {
                index++;
                double num = -1 * (s[index] - '0');
                index++;
                while (s[index] >= '0' && s[index] <= '9' && index < len)
                {
                    num = num * 10 - (s[index] - '0');
                    index++;
                }
                index--;
                a.push(num);
            }
            else if (b.empty())
                b.push(s[index]);
            else
            {
                while (b.size())
                {
                    if (getyou(b.top()) >= getyou(s[index]))
                    {
                        double y = a.top();
                        a.pop();
                        double x = a.top();
                        a.pop();
                        if (b.top() == '+')
                            a.push(x + y);
                        else if (b.top() == '-')
                            a.push(x - y);
                        else if (b.top() == '*')
                            a.push(x * y);
                        else if (b.top() == '/')
                            a.push(x / y);
                        b.pop();
                    }
                    else
                        break;
                }
                b.push(s[index]);
            }
            index++;
        }
        else
        {
            if (s[index] == '(')
                b.push(s[index]);
            else
            {
                while (b.top() != '(')
                {
                    double y = a.top();
                    a.pop();
                    double x = a.top();
                    a.pop();
                    if (b.top() == '+')
                        a.push(x + y);
                    else if (b.top() == '-')
                        a.push(x - y);
                    else if (b.top() == '*')
                        a.push(x * y);
                    else if (b.top() == '/')
                        a.push(x / y);
                    b.pop();
                }
                b.pop();
            }
            index++;
        }
    }
    while (b.size())
    {
        double y = a.top();
        a.pop();
        double x = a.top();
        a.pop();
        if (b.top() == '+')
            a.push(x + y);
        else if (b.top() == '-')
            a.push(x - y);
        else if (b.top() == '*')
            a.push(x * y);
        else if (b.top() == '/')
            a.push(x / y);
        b.pop();
    }
    printf("%.6lf\n",a.top());
}

L2-2不知是吃葡萄不吐葡萄皮的喇嘛拿扁担打了留恋榴莲甜的哑巴一扁担还是留恋榴莲甜的哑巴拿板凳打了吃葡萄不吐葡萄皮的喇嘛一板凳

出题人:勋

题目大意

经典的模式串匹配个数:大小写无关的基础上询问子串在主串中的出现次数。

测试数据

  • 1测试点小数据
  • 2 3 4 5随机大数据,5答案为0
  • 6卡暴力for的break剪枝

解题

究极经典的KMP题,甚至没有一点变形,这里不多于赘述KMP的题解,真不会的同学赶紧去学,有很多更优秀高赞博客。
至于本题的大小写无关纯纯唬人,只需要在输入之后全部转换为小写或者大写即可。

代码

#include <bits/stdc++.h>
using namespace std;
int Next[2000005];
char w[2000005], t[2000005];
void getNext()
{                      //求next数组的函数
    int i = -1, j = 0; //两个下标错开
    Next[0] = -1;      //初始化next[0]=-1
    int lenw = strlen(w);
    while (j < lenw)
    {
        if (i == -1 || w[i] == w[j])
        {
            i++, j++; //相等或i=-1,ij同时加
            if (w[i] == w[j])
                Next[j] = Next[i]; //这一步是个优化,如果相同等于这个相同这个点i的next[i]
            else
                Next[j] = i; //不同直接等于i
        }
        else
            i = Next[i]; //不同从next[i]开始匹配
    }
    return;
}
int Kmp()
{
    int cnt = 0;
    int i = 0, j = 0;
    int lenw = strlen(w);
    int lent = strlen(t);
    getNext(); //得到next数组
    while (j < lent)
    {
        if (i == -1 || w[i] == t[j])
        {
            i++, j++; //类似于getnext函数
            if (i == lenw)
            {
                cnt++; //如果模式字符串到了末尾个数++,可以不用对i进行操作i会通过next数组自己初始回去
            }
        }
        else
            i = Next[i];
    }
    return cnt;
}
int main()
{
    scanf("%s%s", t, w);
    //预处理全部转换为大写/小写
    int lent = strlen(t), lenw = strlen(w);
    for (int i = 0; i < lent; i++)
    {
        if (t[i] >= 'A' && t[i] <= 'Z')
            t[i] += 'a' - 'A';
    }
    for (int i = 0; i < lenw; i++)
    {
        if (w[i] >= 'A' && w[i] <= 'Z')
            w[i] += 'a' - 'A';
    }
    //套用kmp完事
    printf("%d\n", Kmp());
    return 0;
}

L2-3根据二叉树的中序和层序输出树

出题人 : 弛

题目大意

  • 如题根据二叉树的中序和层序输出树

出题报告

  • 测试点有链(全部向左,全部向右),满二叉树,非满二叉树,随机数据等情况

解题思路

  • 先确定根节点,根节点一定是层序遍历的第一个

  • 按照层序的顺序把每个节点装入树上。遍历层序是因为层序可以保证当前节点的父节点已经被装入树中了

  • 但是根据层序遍历无法确定某个点的父亲节点,所以在插入到树上的过程我们需要从根节点开始插入,然后一层一层往下,直到叶子节点停下,成为新的叶子节点

        if (root == -1) {//当前点不存在,插入
            a[tot].data = data;
            a[tot].nextl = -1;
            a[tot].nextr = -1;
            tot++;
            return;
        }
    

    如果root是-1就表示这个点是空的,生成新的叶子节点

  • 剩下需要处理的问题就是怎么正确的一层层往下,怎么知道他最终的位置在当前这个节点(即节点root)的左子树中还是右子树中

  • 观察中序遍历我们可以发现,如果在当前节点的左子树中,那么需要插入的这个节点(data)在中序中的位置(index)应该小于当前节点在中序中的位置

    如果在当前节点的右子树中,那么需要插入的这个节点(data)在中序中的位置(index)应该大于当前节点在中序中的位置

  • 判断一下需要往左子树走还是右子树中走即可,需要注意的是如果当前节点已经是叶子节点了,那么更新完后需要更新nextl或者nextr

代码实现

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x3f3f3f3f
const int maxn = 1100;
int zhong[maxn], cen[maxn];
int plac[maxn];
int n;
struct node {
    int data;
    int nextl, nextr;
}a[maxn];
int tot = 0;
void add(int root, int data, int index) {
    if (root == -1) {//当前点不存在,插入
        a[tot].data = data;
        a[tot].nextl = -1;
        a[tot].nextr = -1;
        tot++;
        return;
    }
    int i=plac[a[root].data];//查找当前根的值在需要插入的点的左边还是右边
    if (i > index) {
        add(a[root].nextl, data, index);
        if (a[root].nextl == -1)a[root].nextl = tot - 1;
    }
    else {
        add(a[root].nextr, data, index);
        if (a[root].nextr == -1)a[root].nextr = tot - 1;
    }
}
void pro(int root) {
    if (root == -1)return;
    cout << a[root].data << " ";
    pro(a[root].nextl);
    pro(a[root].nextr);
}
void hou(int root) {
    if (root == -1)return;
    hou(a[root].nextl);
    hou(a[root].nextr);
    cout << a[root].data << " ";
}
int main() {
    int t;
    cin >> t;
    while (t--) {
        memset(a,0,sizeof(a));
        tot = 1;
        cin >> n;
        for (int i = 0; i < n; i++) {
            cin >> cen[i];//层序
        }
        for (int i = 0; i < n; i++) {
            cin >> zhong[i];//中序
            plac[zhong[i]]=i;
        }
        a[0].data = cen[0];
        a[0].nextl = -1;
        a[0].nextr = -1;
        for (int i = 1; i < n; i++) {
            add(0, cen[i], plac[cen[i]]);
        }
        pro(0);
        cout << endl;
        hou(0);
        cout << endl;
    }
}

L2-4it takes two

出题人:勋

题目大意

给定一个森林,删去其中两个结点及其连边,求剩下边数的最小值。

测试数据

1. 无边
2. 森林
3. $n=100,m=1000$随机数据
4. $n=1000,m=10000$随机数据
5. $n=10000,m=100000$随机数据
6. $n=100000,m=100000$随机数据
7. 最大度多个点卡贪心
8. 同7
9. 卡直接输出最大度的两个点
10. 同9

解题

首先显而易见,在有 m m m条边的图上删去度为 a a a b b b的两个结点,如果这两个结点不相连,则删后的边数为 m − ( a + b ) m-(a+b) m(a+b)。而如果这两个点相连,则删后的边数为 m − ( a + b ) + 1 m-(a+b)+1 m(a+b)+1
所以第一想法如果是贪心选择度最大的两个点删除,只能拿到一部分的分数,因为如果贪心选,当度最大的点有多个时,选择的点是随机的,如果选到的两个点的相连的话答案就会少1。
在这里给出笔者的解题思路(总感觉会有更简单的解法,希望想到的同学可以论证、分享):

  • 首先枚举点作为删的第一个点 x x x设度为 a a a,那删的第二个点 y y y就应当是剩下点的里面最大的度 b b b
  • 这时候只需要看除了 x x x外有着最大度的那些点是否都连在第一个点上,如果是则这种情况最多可以删 a + b − 1 a+b-1 a+b1,否则最多可以删 a + b a+b a+b个。(因为如果在这种情况下,你去删不与 x x x相连的不是最大度的那些点答案也不会更优,读者可以自证)
  • 所以枚举点之后只需要判断"除了 x x x外有着最大度的那些点是否都连在点 x x x"这一个条件。判断方法:与点 x x x相邻的点里有着最大度的个数是否等于除了 x x x外所有点里有着最大度的个数。这样就可以避免枚举之后的冗余判断。
  • 这种判断方法的时间复杂度为 m a x ( n , m ∗ 2 ) max(n,m*2) max(n,m2)

代码

#include<bits/stdc++.h>
#include<unordered_map>
using namespace std;
vector<string> adds;
map<int,int> flag;
map<string,vector<string>> maps;
int main()
{
    
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    int n,m;
    cin>>n>>m;
    int i;
    string s;
    for(int i=0;i<n;i++)
    {
        cin>>s;
        adds.push_back(s);
    }
    int ma=0;
    for(int i=0;i<m;i++)
    {
        string a,b;
        cin>>a>>b;
        maps[a].push_back(b);
        maps[b].push_back(a);
    }
    for(int i=0;i<n;i++)
    {
        string from = adds[i];
        //插入负值,这样auto遍历的时候是访问的最大度
        flag[-maps[from].size()]++;
    }
    for(int i=0;i<n;i++)
    {
        string from = adds[i];
        int maxx,count;
        //maxx表示最大的度数,count表示除x外有着最大度的点数
        for(auto p:flag)
        {
            maxx = -p.first;
            count = p.second;
            if(maps[from].size()==maxx)count--;
            if(count)break;
        }
        int c=0;
        //c表示与x相连的里面最大度的点的个数
        for(auto p:maps[from])
        {
            string to = p;
            if(maps[to].size()==maxx)c++;
        }
        int now = maps[from].size()+maxx;
        if(c==count)
        {
            now--;
        }
        ma=max(ma,now);
    }
    cout<<m-ma;
}

L3

L3-1重生之我是勋总

题意

(出题人觉得题面讲的很详细、很严谨了

解题思路

  • 每次一定是选择一个极大连通块,将里面所有数同时减小,直到最小值变成0,然后将变成0的点删除,分裂成多个连通块再接着做。
  • 为了实现这个策略,可以将整个过程倒过来看,变成按照 b b b的值从大到小依次加入每个点。加入每个点 u u u时遍历与 u u u相连的所有边 ( u , v ) (u, v) (u,v),如果 v v v u u u 之前加入且 u u u v v v不连通则将 u u u v v v合并,并将 v v v所在连通块的树根设为 f a t h e r father father,得到一棵有根树。那么每个点 u u u在成为最小值之前已经被做了 b f a t h e r b_{father} bfather。每次操作,所以每个点 u u u对答案的贡献为 b i − b f a t h e r b_i-b_{father} bibfather
  • 并查集使用路径压缩,时间复杂度 O ( ( n + m ) log ⁡ n ) O((n + m) \log n) O((n+m)logn)

①下列代码中记得区分 f [ i ] f[i] f[i] f a [ i ] fa[i] fa[i]的差别

②得每次加入联通块的点,一定是大于等于当前根的b值的点,这样才会产生贡献。

wake[i]=1代表这个点被访问了,所以它的b值一定是比当前根来的大的

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
int n, m, x, y;
int a[N], q[N], g[N], fa[N], f[N];
bool wake[N];
long long ans;
vector<int> vec[N];
inline bool cmp(int x, int y)
{
    return a[x] > a[y];
}
inline int getfather(int v)
{
    return f[v] == v ? v : f[v] = getfather(f[v]);
}
int main()
{
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        q[i] = f[i] = i;
    }
    sort(q + 1, q + n + 1, cmp);
    while (m--)
    {
        cin >> x >> y;
        vec[x].push_back(y);
        vec[y].push_back(x);
    }
    for (int i = 1; i <= n; i++)
    {
        x = q[i];    //从a[]值最大的节点开始
        wake[x] = 1; //该节点已经标记
        for (int j = 0; j < vec[x].size(); j++)
        {
            y = vec[x][j]; //终点
            if (!wake[y])//没访问过
                continue; 
            y = getfather(y);
            if (y == x)
                continue;
            fa[y] = f[y] = x; //且x和y不连通,则将二者合并,x为父节点
        }
    }
    for (int i = 1; i <= n; i++)
        ans += a[i] - a[fa[i]];
    cout << ans;
}

L3-2数组

出题人 : 举

题意

有一个数组含有n个整数,需要对这个数组进行q次查询。每次查询给定两个整数p和k。把p变成p+a*p+k,直到p大于n

解题思路

如果按照题目进行暴力求解的话,那整体时间复杂度是O( N 2 N^2 N2)。当时k很小时,很显然是不能通过的。

还有一种方法是预处理每种p和k的值,在查询时可以通过O(1)的复杂度得到结果。但因为这题n为100000,所以O( N 2 N^2 N2)的空间复杂度是不可接受的。

这两种思路一个时间复杂度太大,一个空间复杂度太大,那我们可以这两种结合以下,当时 k < n k\lt \sqrt{n} k<n ,第二种思路只需要使用 n n n \sqrt{n} nn 的空间,当 k ≥ n k\ge \sqrt{n} kn 时,第一种思路的单次查询时间复杂度不超过 n \sqrt{n} n

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
const int z = 330;
int dp[z][maxn];
int main() {
	int n;
 
	cin >> n;
	int i, j;
 
	vector<int> v(n + 1);
	for (i = 1; i <= n; i++)cin >> v[i];
	for (j = 1; j < z; j++) {
		for (i = n; i > 0; i--) {
			dp[j][i] = 1;
			if (i + j + v[i] <= n) dp[j][i] += dp[j][i + j + v[i]];
		}
	}
	int q, p, k;
	cin >> q;
	for (i = 0; i < q; i++) {
		cin >> p >> k;
		if (k < z)cout << dp[k][p] << endl;
		else {
			int cnt = 0;
			while (p <= n) {
				p += v[p] + k;
				cnt++;
			}
			cout << cnt << endl;
		}
	}
 
	return 0;
}

L3-3壕勋爱撒钱

出题人 : 杰

题目大意

给定一棵 n n n 个结点的树。 m m m 次操作,每次操作一个起点 s s s 和终点 y y y,使得从 s s s t t t 的简单路径中,第 i 个结点增加 i i i m m m 次操作后,求每个点的权值为多少。对于所有的样例, n < = 5 e 4 , m < = 5 e 5 n<=5e4,m<=5e5 n<=5e4,m<=5e5

测试数据

  • 树的形态为半棵树随机构造,半棵树成一条链。 s s s 随机从链中找一个点, t t t 随机从另外半棵随机构造的树中找一个点。路径长度在 O ( n ) O(n) O(n) 级别。
  • 前两个测试点允许暴力的时间复杂度 O ( n m ) O(nm) O(nm) 通过。 n = 5000 , 5000 n=5000,5000 n=5000,5000
  • 后四个测试点要求允许差分+LCA 的 O ( m l o g n ) O(mlogn) O(mlogn) 通过。 n = 5 e 4 , m = 5 e 5 n=5e4,m=5e5 n=5e4,m=5e5

解题思路

  • 暴力算法: s − t s-t st 的路径可以划分为: s − l c a ( s , t ) − t s-lca(s,t)-t slca(s,t)t ,其中 l c a ( s , t ) lca(s,t) lca(s,t) s s s t t t 的最近公共祖先。 s , t s,t s,t 按谁深度较大谁往上跳的原则,轮流上调,求出他们的最近公共祖先 l c a ( s , t ) lca(s,t) lca(s,t) 。然后轮流分配权值即可。
  • 正确算法LCA+差分:

  • 假设在一个一维数组上,要让 [ l , r ] [l,r] [l,r] 增加 1 , 2 , … … , r − l + 1 1,2,……,r-l+1 1,2,……,rl+1 ,可以通过让二阶差分数组 d 2 d2 d2 中: d 2 [ l ] + = 1 , d 2 [ r + 1 ] − = ( r − l + 2 ) , d 2 [ r + 2 ] + = ( r − l + 1 ) d2[l]+=1,d2[r+1]-=(r-l+2),d2[r+2]+=(r-l+1) d2[l]+=1,d2[r+1]=(rl+2),d2[r+2]+=(rl+1) ,并求 d 2 d2 d2 的二次前缀和实现

  • 例如:

    l=1,r=5
    要得到: 1 2 3 4 5 0 0 0 0 0 的结果
    
    令 d2[l]+=1,d2[r+1]-=6,d2[r+2]+=5
    差分数组为:1 0 0 0 0 -6 5 0 0 0
    一次前缀为:1 1 1 1 1 -5 0 0 0 0
    二次前缀为:1 2 3 4 5  0 0 0 0 0
    
  • 若要在 [ l , r ] [l,r] [l,r] 增加 x , x − 1 , x − 2 , … … , y x,x-1,x-2,……,y x,x1,x2,……,y 呢???

  • 先让 [ l , r ] [l,r] [l,r] 用上诉方法增加 − 1 , − 2 , … … , − ( x − y + 1 ) -1,-2,……,-(x-y+1) 1,2,……,(xy+1),再用一个一阶差分数组 d 1 d1 d1 用于让 [ l , r ] [l,r] [l,r] 增加 x + 1 x+1 x+1 即可。

  • 在该题中,只需将一维数组上的差分转化为树上差分。

  • 具体实现:

  • 维护一个二阶差分数组 d 2 d2 d2 和一个一阶差分数组 d 1 d1 d1

  • l c a lca lca s , t s,t s,t 的最近公共祖先

  • 那么 s , t s,t s,t 之间相当于要增加 [ 1 , 2 , … … , d e p [ t ] + d e p [ s ] − 2 ∗ d e p [ l c a ] + 1 ] [1,2,……,dep[t]+dep[s]-2*dep[lca]+1] [1,2,……,dep[t]+dep[s]2dep[lca]+1]

  • x = d e p [ t ] + d e p [ s ] − 2 ∗ d e p [ l c a ] + 1 , y = d e p [ s ] − d e p [ l c a ] + 1 x=dep[t]+dep[s]-2*dep[lca]+1,y=dep[s]-dep[lca]+1 x=dep[t]+dep[s]2dep[lca]+1,y=dep[s]dep[lca]+1

  • 先解决 s − l c a s-lca slca 这一段

  • d 2 [ s ] + = 1 d2[s]+=1 d2[s]+=1

  • d 2 [ f [ l c a ] ] − = ( y + 1 ) d2[f[lca]]-=(y+1) d2[f[lca]]=(y+1)

  • d 2 [ f [ f [ l c a ] ] ] + = ( y ) d2[f[f[lca]]]+=(y) d2[f[f[lca]]]+=(y)

  • 再解决 t − l c a t-lca tlca 这一段

  • d 2 [ t ] − = 1 d2[t]-=1 d2[t]=1

  • d 2 [ f [ l c a ] ] + = ( x − y + 2 ) d2[f[lca]]+=(x-y+2) d2[f[lca]]+=(xy+2)

  • d 2 [ f [ f [ l c a ] ] ] − = ( x − y + 1 ) d2[f[f[lca]]]-=(x-y+1) d2[f[f[lca]]]=(xy+1)

  • d 1 [ t ] + = ( x + 1 ) d1[t]+=(x+1) d1[t]+=(x+1)

  • d 1 [ f [ l c a ] ] − = ( x + 1 ) d1[f[lca]]-=(x+1) d1[f[lca]]=(x+1)

  • 此时 l c a lca lca 处多加了一个 y y y,用 a [ l c a ] − = y a[lca]-=y a[lca]=y 即可。

  • i i i 个点的答案为: d 2 d2 d2 的二阶前缀和在 i i i 处的值 + d 1 +d1 +d1 的一阶前缀和在 i i i 处的值 + a [ i ] +a[i] +a[i]

代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
vector<int>G[N];
int f[N][25];
long long d2[N],dep[N],d1[N],a[N];
void dfs(int u,int fa){
    f[u][0]=fa;
    dep[u]=dep[fa]+1;
    for(int i=1;i<=20;i++)f[u][i]=f[f[u][i-1]][i-1];
    for(int v:G[u]){
        if(v==fa)continue;
        dfs(v,u);
    }
}
void count1(int u,int fa){
    for(int v:G[u]){
        if(v==fa)continue;
        count1(v,u);
        d1[u]+=d1[v];
    }
}
void count2(int u,int fa){
    for(int v:G[u]){
        if(v==fa)continue;
        count2(v,u);
        d2[u]+=d2[v];
    }
}
int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--){
		if(dep[f[x][i]]<dep[y])continue;
		x=f[x][i];
	}
	if(x==y)return x;
	for(int i=20;i>=0;i--){
		if(f[x][i]==f[y][i])continue;
		x=f[x][i];
		y=f[y][i];
	}
	return f[x][0];
}
int main(){
	int n,m,u,v,s,t;
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
        G[v].push_back(u);
	}
	dfs(1,0);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&s,&t);
		int lca=LCA(s,t);
		int x=dep[t]+dep[s]-2*dep[lca]+1;
		int y=dep[s]-dep[lca]+1;
		d2[s]++;
		if(f[lca][0]){
			d2[f[lca][0]]-=(y+1);
			if(f[lca][1])d2[f[lca][1]]+=(y);
		}
		d2[t]--;
		if(f[lca][0]){
			d2[f[lca][0]]+=(x-y+2);
			if(f[lca][1])d2[f[lca][1]]-=(x-y+1);
		}
		d1[t]+=(x+1);
		if(f[lca][0])d1[f[lca][0]]-=(x+1);
		a[lca]-=y;
	}
	count1(1,0);
	count2(1,0);
	count2(1,0);
	for(int i=1;i<=n;i++){
		cout<<d1[i]+d2[i]+a[i]<<" ";
	}
	return 0;
}

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值