【周赛第70期】4题(2题未测试) 启发式合并 哈希表 最近公共祖先 堆 数学

目录

  • ~~本次比赛前两题似乎没有数据,所以代码可能有隐藏的错误~~
    • TODO 如果用时间的话,准备自己造一些数据测一下。
  • 1、题目名称:小张的手速大比拼
    • 题目
    • 答案
      • 启发式合并
      • 另一种思路(非正解,未经充分测试)
  • 2、题目名称:坐公交
    • 题目
    • 答案
  • 3、题目名称:三而竭
    • 题目
    • 答案
  • 4、题目名称:争风吃醋的豚鼠
    • 题目
    • 答案

本次比赛前两题似乎没有数据,所以代码可能有隐藏的错误

TODO 如果用时间的话,准备自己造一些数据测一下。

1、题目名称:小张的手速大比拼

题目

在很久很久以前,小张找到了一颗有 N 个节点的有根树 T 。 树上的节点编号在 1 到 N 范围内。 他很快发现树上的每个节点 i 都有一个对应的整数值 V[i]。 一个老爷爷对他说,给你一个整数 X, 如果你能回答我的 M 个问题,他就给张浩扬购买一些零食。 对于每个问题 Q[i], 请你找到 在 T 中以节点 Q[i] 为根的子树中的所有节点(包括 Q[i])中, 有没有两个节点 A, B (A != B) 的值 V[A] ^ V[B] 的异或和为 X。 如果有这样的两个节点, 请你输出 YES。 否则你需要输出 NO 表示没有节点符合上面的条件。

答案

启发式合并

关于启发式合并,可以参考: OI Wiki 启发式合并
对于每个子树,以大小最大的子树为基础,每次将根节点或其它较小子树合并进去。
子树 YES,必然有当前树 YES。

不太确定复杂度怎么算,以下不一定对……
大概是考虑所有值被加入集合(哈希表)的总次数,每次O(1)。
对于任意一个节点,它的重子树不需要再次被加入哈希表,其他子树需要再次加入哈希表,最坏情况子树节点数一样大,累计O((子树个数-1)*单个子树节点数)=O(总节点数-单个子树节点数),全加起来是O(总结点数+总结点数*(1-1/子树个数)+总结点数*(1-1/子树个数)^2+...) = O(总结点数*[1+(1-1/子树个数)+(1-1/子树个数)^2+...])
假设树有h层,每层k个分支,则有1+k+k^2+...+k^(h-1) = n = (k^h-1)/(k-1) < k^h
O(n*[1+((k-1)/k)+((k-1)/k)^2+...+((k-1)/k)^(h-1)]) = O(n*[k^h-(k-1)^h]/[k^(h-1)]) = O(n)

#include

using namespace std;
const int N = 101000;
int n, x, m;
vector<int> e[N];
int value[N];
int sz[N], son[N];

void dfs(int u) {
    sz[u] = 1;
    for (const auto &v: e[u]) {
        dfs(v);
        sz[u] += sz[v];
        if (son[u] == 0 || sz[son[u]] < sz[v]) {
            son[u] = v;
        }
    }
}

bool ans[N];

unordered_set<int> work(int u) {
    if (!son[u]) {
        return {value[u]};
    }
    auto now = work(son[u]);
    if (ans[son[u]]) {
        ans[u] = true;
    }
    if (now.count(value[u] ^ x)) {
        ans[u] = true;
    }
    now.insert(value[u]);
    unordered_set<int> tmp;
    for (const auto &v: e[u]) {
        if (v == son[u])continue;
        tmp = work(v);
        if (ans[v]) {
            ans[u] = true;
        }
        if (!ans[u]) {
            for (const auto &it: tmp) {
                if (now.count(it ^ x)) {
                    ans[u] = true;
                    break;
                }
                now.insert(it);
            }
        }
    }
    return now;
}

int main() {
    cin >> n >> x >> m;
    int rt = -1;
    for (int i = 1, fa; i <= n; i++) {
        scanf("%d", &fa);
        if (fa == -1) {
            rt = i;
        } else {
            e[fa].push_back(i);
        }
    }
    for (int i = 1; i <= n; i++)scanf("%d", &value[i]);
    dfs(rt);
    work(rt);
    int q;
    for (int i = 1; i <= m; i++) {
        scanf("%d", &q);
        puts(ans[q] ? "YES" : "NO");
    }
    return 0;
}

另一种思路(非正解,未经充分测试)

树中两个节点的值异或为x,导致包含它们的最近公共祖先的子树返回YES。
所以先标记最近公共祖先,再dfs2标记最近公共祖先直到根节点。

但是如果整个树的节点值只有两种,时间复杂度O(n^2)

#include

using namespace std;

const int N = 101000;
int n, x, m;
vector<int> e[N];
int dep[N], f[N][20];
unordered_map<int, vector<int>> mp;
bool ans[N];

void dfs(int u) {
    for (int i = 1; i < 20; i++) {
        f[u][i] = f[f[u][i - 1]][i - 1];
    }
    for (const auto &v: e[u]) {
        dep[v] = dep[u] + 1;
        f[v][0] = u;
        dfs(v);
    }
}

int getLCA(int u, int v) {
    if (dep[u] < dep[v]) {
        swap(u, v);
    }
    for (int i = 19; i >= 0; i--) {
        if (dep[f[u][i]] >= dep[v]) {
            u = f[u][i];
        }
    }
    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];
}

void dfs2(int u) {
    for (const auto &v: e[u]) {
        dfs2(v);
        ans[u] |= ans[v];
    }
}

int main() {
    cin >> n >> x >> m;
    int rt = -1;
    for (int i = 1, fa; i <= n; i++) {
        scanf("%d", &fa);
        if (fa == -1) {
            rt = i;
        } else {
            e[fa].push_back(i);
        }
    }
    dep[rt] = 1;
    dfs(rt);
    for (int i = 1, value; i <= n; i++) {
        scanf("%d", &value);
        const auto &vList = mp[value ^ x];
        for (const auto &v: vList) {
            ans[getLCA(i, v)] = true;
        }
        mp[value].push_back(i);
    }
    dfs2(rt);
    int q;
    for (int i = 1; i <= m; i++) {
        scanf("%d", &q);
        puts(ans[q] ? "YES" : "NO");
    }
    return 0;
}
/*不是比赛时的用例
5 3 5
-1 1 1 3 3
1 2 1 2 1
1 2 3 4 5
 */

2、题目名称:坐公交

题目

公交上有N排凳子,每排有两个凳子,每一排的凳子宽度不一样。有一些内向和外向的人按照顺序上车。 外向的人(0):只会选择没人的一排坐下,如果有很多排符合要求,他会选择座位宽度最小的坐下。 内向的人(1):只会选择有人的一排坐下,如果有很多排符合要求,他会选择座位宽度最大的坐下。 数据保证存在合理。输出每个人所在的排。

答案

题面应该是把内向外向说反了,明明内向才自己坐……
间接排序得到大小序号。
外向找没人的坐,会按顺序坐,坐完后内向的就可以坐了,所以将大小序号入堆。
内向找有人的最大的坐,也就是将堆顶弹出。O( nlogn )

#include

using namespace std;
const int N = 101000;
int n;
int len[N], r[N];
char s[N * 2];

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &len[i]);
        r[i] = i;
    }
    sort(r + 1, r + n + 1, [](int a, int b) {
        return len[a] < len[b];
    });
//for(int i=1;i<=n;i++){
// cout<
// }
    scanf("%s", s + 1);
    int pos = 0;
    priority_queue<int> q;
    for (int i = 1; i <= n * 2; i++) {
        if (s[i] == '0') {
            ++pos;
            printf("%d ", r[pos]);
            q.push(pos);
        } else {
            int now = q.top();
            q.pop();
            printf("%d ", r[now]);
        }
    }
    return 0;
}

3、题目名称:三而竭

题目

一鼓作气再而衰三而竭。 小艺总是喜欢把任务分开做。 小艺接到一个任务,任务的总任务量是n。 第一天小艺能完成x份任务。 第二天能完成x/k。 。。。 第t天能完成x/(k^(t-1))。 小艺想知道自己第一天至少完成多少才能完成最后的任务。

答案

印象中见过完全一样的题……
份数是整数,相当于每次取整。
第1天x,可以表示为x=a1+a2k+…+ank^(n-1) (0<=ai 第2天 a2+a3k+…+ank^(n-2)

第n天an

总数a1+a2*(k+1)+a3*(k ^2+k+1)+…+an*(k ^(n-1)+…+1) >= n
可以对n取模,每次模(k ^(n-1)+…+1) (k ^(n-2)+…+1) … (k+1) (1)
可以求出an…a1
代入x=a1+a2k+…+ank^(n-1)
可以求出答案。

#include 
#include 
#include 
#include 

using namespace std;
using LL = long long;
const int N = 1010;
LL pk[N];
LL sum[N];

int main() {
    LL n, k;
    cin >> n >> k;
    pk[0] = 1;
    sum[0] = pk[0];
    for (int i = 1; i < N; i++) {
        pk[i] = pk[i - 1] * k;
        sum[i] = sum[i - 1] + pk[i];
    }
    int pos = 0;
    for (; n / sum[pos] >= k; pos++) {
    }
    LL ans = 0;
    for (int i = pos; i >= 0; i--) {
        ans += n / sum[i] * pk[i];
        n %= sum[i];
    }
    cout << ans << endl;
    return 0;
}

4、题目名称:争风吃醋的豚鼠

题目

N个节点两两建边。 不存在3个节点相互之前全部相连。(3个节点连接成环) 最多能建立多少条边?

答案

二分图性质。
一开始画来画去想了半天

#include 
#include 
#include 
#include 

using namespace std;
using LL = long long;

int main() {
    LL n;
    cin >> n;
    cout << (n / 2) * ((n + 1) / 2) << endl;
    return 0;
}

你可能感兴趣的:(ACM,C++,CSDN周赛,散列表,算法,堆,启发式合并,数学)