UOJ Logo gaolinxiang的博客

博客

陈太阳系列题解

2018-10-07 14:47:35 By gaolinxiang

因为两场比赛的 $\text{T1}$ 过水,所以不在此赘述。

全部赛区 Day 1

出题人:$\text{fizzydavid}$。

陈太阳与路径

给定一棵树,求每个点是几条简单路径的中点。树形态随机,$n \le 5 \times 10^5$。

分析

对于此题,只需对于每个点,求出距离它为 $k\ (0\le k \lt n)$ 的点分别有多少个就可以计算答案了。

有两个性质:

  • 随机树的期望平均深度为 $\Theta(\log n)$。
  • 随机树的期望最大深度为 $\Theta(\sqrt n)$。

我们令 $dp1[i][j]$ 表示以结点 $1$ 为根 $i$ 点的子树下距离 $i$ 点为 $j$ 的点的个数。转移十分显然,我们可以用一遍 $\text{DFS}$ 求出它。对于每个结点,$dp1[i][j]\ (j > dep[i])$ 是没有意义的,我们可以以此来压缩时间和空间。具体实现时可以使用 std::vector 以减少空间复杂度。

我们令 $dp2[i][j]$ 表示结点 $i$ 往上走 $j$ 步可能到达的点的数量。这样我们可以再用一遍 $\text{DFS}$ 使用 $dp1$ 求出它。但是发现这样的空间复杂度过大。其实对于此题只需记录一条根到结点的路径上的所有 $dp2$,并在 $\text{DP}$ 过程中计算答案即可。

代码

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn = 5e5 + 5, maxm = 1e6 + 5;
long long ans[maxn];
int n, d[maxn];
int tot, ter[maxm], nxt[maxm], lnk[maxn];
vector<int> dp1[maxn], dp2[maxn];

void addEdge(int u, int v) {
    ter[++tot] = v;
    nxt[tot] = lnk[u];
    lnk[u] = tot;
}

void dfs1(int u, int p) {
    d[u] = 1;
    dp1[u].resize(d[u]);
    dp1[u][0] = 1;
    for (int i = lnk[u], v; i; i = nxt[i]) {
        v = ter[i];
        if (v == p) continue;
        dfs1(v, u);
        d[u] = max(d[u], d[v] + 1);
        dp1[u].resize(d[u]);
        for (int j = 0; j < d[v]; j++) {
            dp1[u][j + 1] += dp1[v][j];
            ans[u] -= 1ll * dp1[v][j] * (dp1[v][j] + 1) / 2;
        }
    }
    for (int i = 0; i < d[u]; i++) {
        ans[u] += 1ll * dp1[u][i] * (dp1[u][i] + 1) / 2;
    }
}

void dfs2(int u, int p, int x) {
    for (int i = 1; i < d[u] && i < dp2[x].size(); i++) {
        ans[u] += 1ll * dp1[u][i] * dp2[x][i];
    }
    for (int i = lnk[u], v; i; i = nxt[i]) {
        v = ter[i];
        if (v == p) continue;
        dp2[x + 1].resize(d[v] + 1);
        dp2[x + 1][1] = 1;
        for (int j = 1; j < d[v]; j++) {
            dp2[x + 1][j + 1] = (j < dp2[x].size() ? dp2[x][j] : 0) + dp1[u][j] - dp1[v][j - 1];
        }
        dfs2(v, u, x + 1);
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 1, u, v; i < n; i++) {
        scanf("%d %d", &u, &v);
        addEdge(u, v), addEdge(v, u);
    }
    dfs1(1, 0);
    dfs2(1, 0, 0);
    for (int i = 1; i <= n; i++) {
        printf("%lld%c", ans[i], " \n"[i == n]);
    }
    return 0;
}

陈太阳与开关

给定一棵 $n$ 个结点的树,求有多少个集合,使得在集合中的点连出的边的存在性取反后新生成的图联通。

分析

发现不联通的情况很少。考虑计算不联通的情况。

称集合中出现的点为白点,集合中未出现的点为黑点。

如果有相邻的两个点同色,不妨设他们是黑点。发现所有白点与这两个点联通(他们与这两个点中至少一个不相邻)。我们现在只需考虑是否有黑点与这两个点不联通。

发现一个黑点与这两个点不联通当且仅当所有白色的点围住了它,不然的话他一定可以与某个白色点联通。我们称这种点为孤立点。那么这种情况下联通的充要条件就是不存在孤立点。枚举孤立点后特判去重即可。

当没有相邻两个点同色时,直接模拟两种情况并用并查集优化。也可以采用 $\text{\_rqy}$ 神仙的做法:还是使用枚举孤立点的方法。发现只有一种反例:

          O----------O
         / \        / \
        /   \      /   \
       /     \    /     \
      +-------+  +-------+

特判掉即可。

当然,对于小规模的数据也需要特判,否则会被直接卡到 $50$ 分。

代码

$\%\%\%$ 神仙 $\text{\_rqy}\ !$

#include <cstdio>

const int maxn = 1e5 + 5, maxm = 2e5 + 5, mod = 1e9 + 7;
int T, n, m, a[maxn], u[maxn], v[maxn];
int tot, ter[maxm], nxt[maxm], lnk[maxn], deg[maxn];
int bin[maxn], vis[maxn];

void addEdge(int u, int v) {
    ter[++tot] = v;
    nxt[tot] = lnk[u];
    lnk[u] = tot;
    deg[u]++;
}

bool check() {
    for (int i = 1; i < n; i++) {
        if (deg[u[i]] == 2 && deg[v[i]] == 2) {
            int a = 0, b = 0;
            for (int j = lnk[u[i]]; j; j = nxt[j]) {
                if (ter[j] != v[i]) {
                    a = ter[j];
                    break;
                }
            }
            for (int j = lnk[v[i]]; j; j = nxt[j]) {
                if (ter[j] != u[i]) {
                    b = ter[j];
                    break;
                }
            }
            if (deg[a] + deg[b] == n - 2 && deg[a] >= 2 && deg[b] >= 2) {
                return 1;
            }
        }
    }
    return 0;
}

int solve() {
    for (int i = 1; i <= n; i++) {
        vis[i] = 0;
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        if (deg[i] == 1) {
            ans += !vis[ter[lnk[i]]];
            vis[ter[lnk[i]]] = 1;
        } else {
            ans++;
        }
    }
    return ans;
}

int main() {
    scanf("%d", &T);
    bin[0] = 1;
    for (int i = 1; i <= 1e5; i++) {
        bin[i] = bin[i - 1] + bin[i - 1];
        bin[i] < mod ? NULL : bin[i] -= mod;
    }
    while (T--) {
        scanf("%d", &n);
        tot = 0;
        for (int i = 1; i <= n; i++) {
            lnk[i] = deg[i] = 0;
        }
        for (int i = 1; i < n; i++) {
            scanf("%d %d", &u[i], &v[i]);
            addEdge(u[i], v[i]), addEdge(v[i], u[i]);
        }
        int cnt = 0;
        for (int i = 1; i <= n; i++) {
            if (deg[i] == 1) {
                cnt++;
            }
        }
        if (n == 1 || n == 2) {
            puts("2");
        } else if (cnt == n - 1) {
            printf("%d\n", (bin[n] - 2 + mod) % mod);
        } else if (cnt == n - 2) {
            printf("%d\n", (bin[n] - 6 + mod) % mod);
        } else if (check()) {
            printf("%d\n", (bin[n] - 14 + mod) % mod);
        } else {
            printf("%d\n", (bin[n] - 2 * solve() + mod) % mod);
        }
    }
    return 0;
}

青岛赛区 Day 2

出题人:$\text{apiadu}$。

陈太阳与直径

求 $n$ 个点的带标号树直径长度为 $0, 1, 2, \cdots, n-1$ 的个数 $\text{mod }m$ 的结果。$m$ 是素数,$n \le 500$。

分析

令 $f_{i, j}$ 表示 $i$ 个点的带标号树,深度 $\le j$ 的有多少个。转移方程如下:


$$f_{i, j} = i \times \sum_{k = 1}^{i - 1} {\binom{i - 2}{k - 1} \times f_{k, j - 1} \times \dfrac{f_{i - k, j}}{i - k}}$$


解释:将一棵结点数为 $k$ 的树接到一棵结点数为 $i - k$ 的树的根结点下面。要 $\div (i - k)$ 是因为那棵结点数为 $i - k$ 的树的根结点是确定的,乘的组合数是将除了两棵树的根结点以外的两棵树中的结点按照相对顺序排列的方案数。

令 $f_{i, j}$ 表示 $i$ 个点的带标号树,深度 $= j$ 的有多少个。显然,$g_{i, j} = f_{i, j} - f_{i, j - 1}$。

考虑如何计算答案。当树的直径长度为 $2k+1$,则树的中心为一条边。他两边连接的两棵子树的深度为 $k$。于是答案为:


$$\sum_{j = 1}^{n - 1} {\binom{n - 1}{j - 1} \times g_{j, k} \times g_{n - j, k}}$$


当树的直径长度为 $2k$,则树的中心为一个点。于是深度 $k$ 出现了至少两次。此时答案为:


$$g_{n, k} - \sum_{j = 1} ^ {n - 1} \binom{n}{j} \times f_{j, k - 1} \times f_{n - j,k - 1}$$


即所有深度为 $k$ 的树减去深度 $k$ 只出现了一次的树。

注意在 $n$ 较小时进行特判。时间复杂度 $\Theta(n ^ 3)$。(好像可以 $\text{FFT}$ ?)

代码

第一份是 $90$ 分代码。它较为容易理解,但是常数较大。

#include <cstdio>

const int maxn = 505;
int n, mod, inv[maxn], a[maxn], c[maxn][maxn];
int f[maxn][maxn], g[maxn][maxn], d[maxn][maxn];

int qpow(int a, int b) {
    int c = 1;
    for (; b; b >>= 1, a = 1ll * a * a % mod) {
        if (b & 1) {
            c = 1ll * a * c % mod;
        }
    }
    return c;
}

int main() {
    scanf("%d %d", &n, &mod);
    if (n == 1) {
        return puts("1"), 0;
    } else if (n == 2) {
        return puts("0 1"), 0;
    }
    for (int i = 0; i <= n; i++) {
        inv[i] = qpow(i, mod - 2);
    }
    for (int i = 0; i <= n; i++) {
        c[i][0] = c[i][i] = 1;
        for (int j = 1; j < i; j++) {
            c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
            c[i][j] < mod ? NULL : c[i][j] -= mod;
        }
    }
    d[1][0] = 1;
    for (int i = 0; i <= n; i++) {
        f[1][i] = 1;
    }
    for (int i = 2; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            for (int k = 0; k < i; k++) {
                f[i][j] = (f[i][j] + 1ll * f[k][j - 1] * f[i - k][j] % mod * inv[i - k] % mod * c[i - 2][k - 1]) % mod;
            }
            f[i][j] = 1ll * i * f[i][j] % mod;
            d[i][j] = f[i][j] - f[i][j - 1];
            d[i][j] < 0 ? d[i][j] += mod : NULL;
            for (int j = i; j <= n; j++) {
                f[i][j] = f[i][i - 1];
            }
        }
    }
    for (int i = 2; i < n; i++) {
        if (i & 1) {
            for (int j = 1; j < n; j++) {
                a[i] = (a[i] + 1ll * d[j][i / 2] * d[n - j][i / 2] % mod * c[n - 1][j - 1]) % mod;
            }
        } else {
            a[i] = d[n][i / 2];
            for (int j = 1; j < n; j++) {
                a[i] = (a[i] + mod - 1ll * d[j][i / 2 - 1] * f[n - j][i / 2 - 1] % mod * c[n][j] % mod) % mod;
            }
        }
    }
    for (int i = 0; i < n; i++) {
        printf("%d%c", a[i], " \n"[i == n - 1]);
    }
    return 0;
}

第二份是 $\text{AC}$ 代码。加了一些小优化。

#include <cstdio>

const int maxn = 505;
int n, mod, inv[maxn], a[maxn], c[maxn][maxn];
int f[maxn][maxn], d[maxn][maxn];
long long g[maxn][maxn];

int qpow(int a, int b) {
    int c = 1;
    for (; b; b >>= 1, a = 1ll * a * a % mod) {
        if (b & 1) {
            c = 1ll * a * c % mod;
        }
    }
    return c;
}

int main() {
    scanf("%d %d", &n, &mod);
    long long inf = 5000000000000000000ll / mod * mod;
    if (n == 1) {
        return puts("1"), 0;
    } else if (n == 2) {
        return puts("0 1"), 0;
    }
    for (int i = 1; i <= n; i++) {
        inv[i] = qpow(i, mod - 2);
    }
    for (int i = 0; i <= n; i++) {
        c[i][0] = c[i][i] = 1;
        for (int j = 1; j < i; j++) {
            c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
            c[i][j] < mod ? NULL : c[i][j] -= mod;
        }
    }
    d[1][0] = 1;
    for (int i = 0; i <= n; i++) {
        f[1][i] = g[1][i] = 1;
    }
    for (int i = 2; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            for (int k = 0; k < i; k++) {
                g[i][j] += 1ll * f[k][j - 1] * g[i - k][j] % mod * c[i - 2][k - 1];
                if (g[i][j] >= inf) g[i][j] -= inf;
            }
            g[i][j] %= mod;
            f[i][j] = 1ll * i * g[i][j] % mod;
            d[i][j] = f[i][j] - f[i][j - 1];
            d[i][j] < 0 ? d[i][j] += mod : NULL;
            for (int j = i; j <= n; j++) {
                f[i][j] = f[i][i - 1];
                g[i][j] = g[i][i - 1];
            }
        }
    }
    for (int i = 2; i < n; i++) {
        if (i & 1) {
            for (int j = 1; j < n; j++) {
                a[i] = (a[i] + 1ll * d[j][i / 2] * d[n - j][i / 2] % mod * c[n - 1][j - 1]) % mod;
            }
        } else {
            a[i] = d[n][i / 2];
            for (int j = 1; j < n; j++) {
                a[i] = (a[i] + mod - 1ll * d[j][i / 2 - 1] * f[n - j][i / 2 - 1] % mod * c[n][j] % mod) % mod;
            }
        }
    }
    for (int i = 0; i < n; i++) {
        printf("%d%c", a[i], " \n"[i == n - 1]);
    }
    return 0;
}

陈太阳与酒店

原来的题意太长了,请自行看题。

题意可以转化为:给定一个点带权的二分图,左侧有 $n$ 个点,权值为 $a_1, a_2, a_3, \cdots, a_n$,右侧有 $m$ 个点,权值为 $b_1, b_2, b_3, \cdots, b_m$。左侧第 $i$ 个点向右侧第 $j$ 个点连边当且仅当 $l_i \le j \le r_i$。求它的一个独立集,使得不在独立集中的点权值的平均值最大。题意的转化过程请读者自行思考。

分析

首先想到 $01$ 分数规划(二分答案)。二分 $x$,使每个点的权值 $- x$。我们要求新图中的最小独立集。为了方便起见,我们把每个点的权值取反,这样只需计算最大独立集。

考虑 $\text{DP}$。令 $dp_i$ 表示考虑到右侧第 $i$ 个点的最大权值和。我们发现对于左边的点 $k$,如果右侧的点中区间 $[l_k, r_k]$ 都没被选,则它就可以被选。预处理出 $f_{x, y}$ 表示如果右侧的点中区间 $[x, y]$ 都没被选那么左边的点中能选到的最大权值和为多少。这样 $dp_{i} = b_i + \max_{j = 0}^{i - 1} {dp_j + f_{j + 1, i - 1}}$。于是我们就得到了一个 $\Theta(n^2)$ 的算法。

考虑使用线段树优化这个算法。当 $\text{DP}$ 完右侧第 $i$ 个点时,我们把所有 $r_j = i$ 的左侧第 $j$ 个点添加到线段树中。因为 $dp_0, dp_1, dp_2, \cdots, dp_{l_j - 1}$ 转移到 $dp_{i + 1}, dp_{i + 2}, \cdots, dp_{m}$ 都要加上左侧第 $j$ 个点的权值,所以我们给线段树的 $[0, l_j)$ 加上 $a_j$。$dp_i$ 的值可通过查询线段树上 $[0, i - 1]$ 的获取。于是我们就得到了一个 $\Theta(n\log_2n)$ 的做法。

代码

首次尝试了用整形变量实现 $01$ 分数规划。这样可以有效避免精度问题并提高运算效率。

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

#define lson (root << 1)
#define rson (lson | 1)
#define mid ((l + r) >> 1)
const int maxn = 30005;
const int maxm = 66666;
const long long mult = 1000000;
int n, m, a[maxn], b[maxn], l[maxn], r[maxn];
vector<int> v[maxn];
long long A[maxn], B[maxn];
long long mx[maxm], tag[maxm];

void update(int root) {
    mx[root] = max(mx[lson], mx[rson]);
}

void build(int root, int l, int r) {
    tag[root] = 0;
    if (l == r) {
        mx[root] = 0;
        return;
    }
    build(lson, l, mid);
    build(rson, mid + 1, r);
    update(root);
}

void pushDown(int root) {
    if (tag[root]) {
        mx[lson] += tag[root];
        tag[lson] += tag[root];
        mx[rson] += tag[root];
        tag[rson] += tag[root];
        tag[root] = 0;
    }
}

void modify(int root, int l, int r, int lx, int rx, long long d) {
    if (l == lx && rx == r) {
        mx[root] += d;
        tag[root] += d;
        return;
    }
    pushDown(root);
    if (rx <= mid) {
        modify(lson, l, mid, lx, rx, d);
    } else if (lx > mid) {
        modify(rson, mid + 1, r, lx, rx, d);
    } else {
        modify(lson, l, mid, lx, mid, d);
        modify(rson, mid + 1, r, mid + 1, rx, d);
    }
    update(root);
}

long long query(int root, int l, int r, int lx, int rx) {
    if (l == lx && rx == r) {
        return mx[root];
    }
    pushDown(root);
    if (rx <= mid) {
        return query(lson, l, mid, lx, rx);
    } else if (lx > mid) {
        return query(rson, mid + 1, r, lx, rx);
    } else {
        return max(query(lson, l, mid, lx, mid), query(rson, mid + 1, r, mid + 1, rx));
    }
}

long long solve(long long x) {
    long long sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += 1ll * mult * a[i] - x;
        A[i] = max(0ll, x - 1ll * mult * a[i]);
    }
    for (int i = 1; i <= m; i++) {
        sum += 1ll * mult * b[i] - x;
        B[i] = max(0ll, x - 1ll * mult * b[i]);
    }
    build(1, 0, m);

    for (int i = 1; i <= m; i++) {
        long long x = query(1, 0, m, 0, i - 1) + B[i];
        modify(1, 0, m, i, i, x);
        for (int j = 0; j < v[i].size(); j++) {
            modify(1, 0, m, 0, l[v[i][j]] - 1, A[v[i][j]]);
        }
    }
    return sum + query(1, 0, m, 0, m);
}

int main() {
    scanf("%d %d %*d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
    }
    for (int i = 1; i <= m; i++) {
        scanf("%d", &b[i]);
    }
    for (int i = 1; i <= n; i++) {
        scanf("%d %d", &l[i], &r[i]);
        v[r[i]].push_back(i);
    }
    long long L = 0, R = 100000 * mult, M;
    while (L < R) {
        M = (L + R + 1) >> 1;
        if (solve(M) >= 0) {
            L = M;
        } else {
            R = M - 1;
        }
    }
    printf("%lld.%06lld\n", L / mult, L % mult);
    return 0;
}

8.20 范里吉吉纪念赛 T3 吉吉游西藏 题解

2018-08-21 12:18:05 By gaolinxiang

Subtask #1:$n, m \le 1000 \ (30 \ pts)$

直接递推。令 $f[i][j]$ 表示从起点走到 $(i,j)$ 的方案数。使用 $f[i][j]=f[i-1][j]+f[i][j-1]$ 递推即可。注意本题求的是 $233^{f[n-1][m-1]}$ 的值,所以我们需要计算的是 $f[n-1][m-1] \bmod 999911658$ 的值,而不是 $f[n-1][m-1] \bmod 999911659$ 的值。时间复杂度 $\Theta(nm)$。提交记录:http://zhengruioi.com/submission/31143

Subtask #2:$n=m,k=0 \ (20 \ pts)$

观察到这种网格如果没有额外危险点,从 $(0,0)$ 走到 $(n-1,n-1)$ 的方案数为卡特兰数的第 $n-1$ 项。而我们要计算的是答案 $\bmod 999911658 = 2 \times 3 \times 4679 \times 35617$ 的结果,所以只需对每个素因子分别使用 $\text {Lucas}$ 计算组合数,然后将答案使用中国剩余定理合并即可。结合暴力可得 $50 \ pts$。

Subtask #3:$k=0 \ (20 \ pts)$

这个数据点要求我们理解卡特兰数的本质。当然如果你在 $\text C$ 班好好听讲也可以作出这个点。我们考虑从 $(x_0, y_0)$ 走到 $(x_1, y_1)$ 的方案数。我们考虑先计算出没有 $x>y$ 不能走的限制的方案数,再减去不合法的方案数。总方案数显然是:

$$\binom{x_1-x_0+y_1-y_0}{x_1-x_0}$$

对于不合法的方案,我们考虑这个路径。它一定穿过一条 $x = y$ 的直线。也就是说,他一定与这条直线有交点。

*-*-*-*-*-*-*
 \| | | | | |
* *-*-*-*-*-*
   \| | | | |
* * *-*-*-*-*
     \| | | |
* * * *-*-*-*
       \| | |
* * * * *-*-*

我们考虑将这条路径和这条直线的第一个交点取出,然后将这条路径中这个交点之前的路径全部以这条直线为对称轴翻转过来。这样,每条路径就队对应了从一个新的起点(原起点以直线为对称轴后翻转得到的点)和原终点的所有路径条数。这样的路径条数为:

$$\binom{x_1-x_0+y_1-y_0}{y_1-y_0+(y_0-x_0+1)}$$

于是我们就可以用刚才 $\text {Subtask #2}$ 中介绍的的技巧算出这个值了。结合前述算法可得 $70 \ pts$。提交记录:http://zhengruioi.com/submission/31162

Subtask #3 的另类做法

由于数据随机,并且最大模数只有 $35617$,所以组合数到一定范围 $\bmod 35617$ 就等于 $0$ 了。于是我们只需要输出 $233^0=1$ 即可获得 $40 \ pts$。这个故事告诉我们:

  • 人是要有信仰的
  • 出题人生成数据是不应该纯随机,否则会出事

Subtask #4 $(n,m \le 10^9, k \le 1000)$

我们考虑容斥。先令从 $(x_0, y_0)$ 走到 $(x_1, y_1)$ 的方案数为 $paths(x_0, y_0, x_1, y_1)$。令 $f[i]$ 表示到达第 $i$ 个危险点,并且不到达其他的危险点的总方案数。转移方程:$f[i]=paths(0,0,x_i,y_i)-\sum_{x_j\le x_i, y_j\le y_i}paths(x_j, y_j, x_i, y_i)$。于是,我们就可以在 $\Theta(k^2)$ 的时间内解决这个问题了。提交记录:http://zhengruioi.com/submission/31256

总结

这个题是一道数论五合一,完美的覆盖了 $\text {C}$ 班上课的知识点,是一道综合性的好题。希望这道题能为您的正睿 $\text {OI}$ 暑假学习之旅画上 $\text {van}$ 美的句号。

Zhuky_RLG Round #1 题解

2018-08-16 21:59:57 By gaolinxiang

不要忘了点好评!

Task #1 「ZYQ 的神秘家谱 | Family Tree」

Subtask #1:$n,m\le 100\ (60\ pts)$

将每个点与之能够直接到达的点连边,然后使用 $\text{Floyd}$(传递闭包)预处理出每个点能到达那些点,直接计算即可。时间复杂度 $\Theta(n^3)$。

Subtask #2:$n,m\le 3000\ (100\ pts)$

发现这个图是一个 $\text{DAG}$,然后使用拓扑排序就可以对于每个点可以 $\Theta(n)$ 算出他能够到达那些点。时间复杂度 $\Theta(n^2)$。

#include <cstdio>
#include <iostream>
#include <string>
#include <map>
#include <queue>
using namespace std;

const int maxn = 3005;
const int logn = 18;
int n, m, q, cnt, du[maxn];
int tot, ter[maxn], nxt[maxn], lnk[maxn];
bool ok[maxn][maxn];
map<string, int> mmp;
queue<int> que;
string fa, ch;

void addedge(int u, int v) {
    ter[++tot] = v;
    nxt[tot] = lnk[u];
    lnk[u] = tot;
    du[v]++;
}

int main() {
    ios::sync_with_stdio(0);
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++) {
        cin >> fa;
        mmp[fa] = ++cnt;
    }
    for (int i = 1; i <= m; i++) {
        cin >> fa >> ch;
        addedge(mmp[ch], mmp[fa]);
    }
    for (int i = 1; i <= n; i++) {
        if (du[i] == 0) {
            que.push(i);
        }
        ok[i][i] = 1;
    }
    while (!que.empty()) {
        int u = que.front();
        que.pop();
        for (int i = lnk[u]; i; i = nxt[i]) {
            int v = ter[i];
            for (int j = 1; j <= n; j++) {
                ok[v][j] |= ok[u][j];
            }
            if (--du[v] == 0) {
                que.push(v);
            }
        }
    }
    while (q--) {
        cin >> fa >> ch;
        puts(ok[mmp[fa]][mmp[ch]] && mmp[fa] != mmp[ch] ? "YES" : "NO");
    }
    return 0;
}
Subtask #3:$n,m\le 100000\ (0\ pts)$

如果一个人只有一个爸爸,我们就可以发现这个图是一棵内向树组成的森林,询问就变成了 $u$ 是否在 $v$ 的子树中。将每棵树 $\text{DFS}$ 一遍,求出每个节点子树中 $\text{DFS}$ 序的最小值和最大值。于是判断 $u$ 是否在 $v$ 的子树中就变成了判断 $lftbnd[v]\le dfn[u]\le rhtbnd[v]$ 是否成立。时间复杂度 $\Theta(n)$。

Task #2 「GWQ 与 MC 决胜 | Minecraft Duel」

Subtask #1:$n,m\le 100\ (20\ pts)$

枚举左端点和右端点,然后暴力 $\text{check}$ 即可。时间复杂度 $\Theta(n^3)$。

Subtask #2:$n,m\le 2000\ (50\ pts)$

枚举左端点和右端点,一边枚举一边 $\text{check}$ 即可。时间复杂度 $\Theta(n^2)$。

Subtask #3:$n,m\le 100000\ (100\ pts)$

使用 $\text{2-pointers}$,即可将复杂度优化到 $\Theta(n)$。

#include <cstdio>
#include <iostream>
using namespace std;

const int maxn = 200005;
int cur, n, m, k, a[maxn], b[maxn], c[maxn];
long long s;

void update(int x, int y) {
    if (c[x] >= b[x] && c[x] + y < b[x]) cur--;
    if (c[x] < b[x] && c[x] + y >= b[x]) cur++;
    c[x] += y, s += y * x;
}

int main() {
    scanf("%d %d %d", &n, &m, &k);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]), a[i + n] = a[i];
    }
    for (int t, i = 1; i <= m; i++) {
        scanf("%d", &t), b[t]++;
    }
    int p = 1;
    for (int i = 1; i <= n; i++) {
        update(a[i], 1);
    }
    int ans = 0;
    for (int i = n + 1; i <= n * 2; i++) {
        update(a[i], 1);
        while (i - p >= n || s >= k) {
            update(a[p++], -1);
        }
        ans = max(ans, cur);
    }
    printf("%d\n", ans);
    return 0;
}

Task #3 「ZYQ 与他的 Diep 游戏 | Diep」

Subtask #1:$n\le 2\ (40\ pts)$

直接枚举两行分别打到第几个怪物即可。时间复杂度 $\Theta(1)$。

Subtask #2:$n\le 8\ (100\ pts)$

状压 $\text{DP}$。$dp[mask]$ 表示每排打 $0,1,2,3,4$ 时剩余的最大子弹数量。时间复杂度 $\Theta(5^n)$。具体细节见代码。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

const int maxn = 10;
const int maxm = 390630;
int n, m, a[maxn][5], pw[maxn], dp[maxm];

int main() {
    pw[0] = 1;
    for (int i = 1; i <= 8; i++) {
        pw[i] = pw[i - 1] * 5;
    }
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int j = 4; j >= 1; j--) {
            scanf("%d", &a[i][j]);
            if (a[i][j] == 3) {
                a[i][j] = -1;
            } else if (a[i][j] == 4) {
                a[i][j] = 1;
            } else {
                a[i][j] = -2;
            }
        }
    }
    dp[0] = m;
    for (int i = 1; i < pw[n]; i++) {
        for (int j = 1; j <= n; j++) {
            int t = i / pw[j - 1] % 5;
            if (t && dp[i - pw[j - 1]]) {
                dp[i] = max(dp[i], dp[i - pw[j - 1]] + a[j][t]);
            }
        }
    }
    if (dp[pw[n] - 1]) {
        puts("YES");
        return 0;
    }
    for (int i = 1; i <= n; i++) {
        if (a[n][i] < 0 && dp[pw[n] - 1 - pw[i - 1]] >= -a[n][i]) {
            puts("YES");
            return 0;
        }
    }
    puts("NO");
    return 0;
}
Subtask #3:$n\le 2000\ (100\ pts)$

我们发现最优方案肯定是每次打掉最右边的一个怪物。直接 $\Theta(n^2)$ 枚举打那些怪物即可。

对出题人的建议:

  • 本次比赛题目的细节非常多,但是思维难度非常低,完全不符合 $\text{CCF-NOIP}$ 的尿性出题标准,导致比赛全场出现很多爆零的情况,使得出题人不得不提前报出不爆零的名单。希望出题人进行深刻反思。
  • 不过总之:

    题出的好!覆盖知识点广,题目又着切合实际的背景,解法比较自然。给出题人点赞!出题人精心构造数据,防止了好多同学因为各种奇怪的乱搞搞到了较高的分数,具有较高的区分度。分数段分布设置合理,每个题目都给了巧妙的暴力分。出题人沥尽心血,精心打造的 $CCF-NOIP$ 模拟题,得到了前所未有的效果。这是给我印象最深刻的一次模拟比赛!

记一次数组开小引发的悲剧

2018-08-10 19:12:05 By gaolinxiang

写一篇 Blog 来记录今天比赛的爆炸。


模拟赛:

$8:35$:开始考试。

$8:50$:已经读完题了。$Task\ \#1$ 似乎是一个细节很多的题目,我不是很会写。$Task\ \#2$ 十分可做,感觉自己想到的算法是正解(讲评的时候发现的确是正解)。立刻开始码码码。

$9:30$:码完 $Task\ \#2$,开始测试。小样例一次过,为什么大样例过不去?

$10:55$:查 Tarjan,查组合数,都没发现问题。终于发现了其实是建图中的错误。马上改掉。

$11:00$:调过大样例,大致再看了一下。

$11:03$:信心满满的提交!(然鹅并不知道出锅了……)

$11:25$:写完 $Task\ \#3$ 的 $40+10=50$ 分暴力,立刻提交。想想 $Task\ \#1$ 怎么写吧。

$11:30$:似乎细节比较多,十分难写呀。。。先写个暴力,再找找规律吧。

$11:50$:暴力写完。开始找规律。

$12:00$:发现如果找循环节的话超级难写(没想到倍增),于是决定交暴力。

之后:放松???

$13:15$:再检查一下吧。

$13:20$:发现 $Task\ \#3$ 的链的部分分似乎没有说链的生成方式。这就说明链的端点不一定是 $1$ 和 $n$。赶紧改!

$13:32$:提交,有惊无险。

$13:35$:结束考试,去吃饭。


饭后:

发现自己 $Task\ \#2$ 爆炸了,只有 $30$ 分。

立刻查找错误。然后发现 $OJ$ 上的一个非常小的测试数据,复制下来,用程序跑了一遍。什么?我怎么本机评测 $A$ 了?

立刻去和同学理论,无果。去问老师,老师却惊讶于我居然过了第 $10$ 个点。这数据得有多水呀。。。

蔡老板说:“你太菜了,活该被评测机 ‘制裁’ ”。

再看 $Task\ \#3$,发现我的第一次提交已经有 $50$ 分了。早知道最后就不再交一次了,白白损失了 $2h+$ 的罚时。

某同学:“我 $Task\ \#3$ 乱搞了 $75$ 分!” 强啊!

过了一会儿,成绩出来了。$25$ 名,还不错。$Rating$ 掉到了正好 $2000$(这个数字真好啊)。


讲评:

发现 $Task\ \#1$ 只要拆点就比较好做了。$Task\ \#2$ 我的做法和标算一摸一样。$Task\ \#3$ 似乎是数学题?

毛老师强行 推销。


订正:

发现 $Task\ \#2$ 数组开小了。

顿时心中 ©˙¨¥@#$©¨ø@#¨©%√≈&å´^&!∂˜∆∑˙!#´∫3¨@√ß˚˜ß∂˚∆……


总结:

以后打比赛时应该多花时间在检查方面,以免出现不必要的锅,考完后后悔莫及。

2018 暑期集训 AB 班刷题营 Day2 题解

2018-08-05 13:25:04 By gaolinxiang

不要忘了点好评!

Update #1:Task #2 题解已更新。

Update #2:Task #3 题解已更新。已经全部更新完毕。

Task #1:占领地区 Occupy

Subtask #1:$n,m\le3000\ (50\ pts)$

直接暴力对网格图进行覆盖即可。

#include <cstdio>
const int subn = 3005;
int n, m, b[subn][subn];
int main() {
    scanf("%d %d", &n, &m);
    for (int x, y, k = 1; k <= m; k++) {
        scanf("%d %d", &x, &y);
        for (int i = x, j = y; i > 0 && j > 0; i--, j--) {
            b[i][j] = 1;
        }
        for (int i = x, j = y; i > 0 && j <= n; i--, j++) {
            b[i][j] = 1;
        }
        for (int i = x, j = y; i <= n && j > 0; i++, j--) {
            b[i][j] = 1;
        }
        for (int i = x, j = y; i <= n && j <= n; i++, j++) {
            b[i][j] = 1;
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            ans += !b[i][j];
        }
    }
    printf("%d\n", ans);
    return 0;
}
Subtask #2:$n\le100000,m\le3000\ (70\ pts)$

将两种对角线分开处理,将总答案分别减去这两种对角线的答案,再加上两种对角线重合的格子数量即可。直接计算的时间复杂度为 $\Theta(n+m^2)$。

Subtask #3:$n,m\le100000\ (100\ pts)$

如何快速计算出两种对角线重合的格子数量呢?枚举每一个 $x-y$ 为恒定值的对角线,用前缀和计算有几个 $x+y$ 为恒定值的对角线与之重合即可。时间复杂度 $\Theta(n+m)$。具体细节见代码。

#include <cstdio>
const int maxn = 100005;
const int maxm = 200005;
int n, m, bs[maxm], bd[maxm];
int abs(int x) {
    return x < 0 ? -x : x;
}
int main() {
    scanf("%d %d", &n, &m);
    for (int x, y, i = 1; i <= m; i++) {
        scanf("%d %d", &x, &y);
        bs[x + y] = 1;
        bd[x - y + n] = 1;
    }
    long long ans = 1ll * n * n;
    for (int i = 2; i <= 2 * n; i++) {
        if (bs[i]) {
            ans -= n - abs(n + 1 - i);
        }
    }
    for (int i = 1; i < 2 * n; i++) {
        if (bd[i]) {
            ans -= n - abs(n - i);
        }
    }
    for (int i = 2; i <= 2 * n; i++) {
        bs[i] += bs[i - 2];
    }
    for (int i = 1; i < 2 * n; i++) {
        if (bd[i]) {
            int x = abs(n - i);
            ans += bs[2 * n - x] - bs[x];
        }
    }
    printf("%lld\n", ans);
    return 0;
}

Task #2:匹配 Match

Subtask #1:$n,m\le4\ (20\ pts)$

直接暴力枚举同学的位置即可。

Subtask #2:$n,m\le300\ (50\ pts)$

我们考虑对于每条边计算总共经过这条边的次数。我们发现对于每种配对方案,我们肯定是将一条边左边的 $男\ /\ 女$ 同学有先和右边的 $女\ /\ 男$ 同学配对。我们设这条边断开后两个联通块的大小为 $size_0$ 和 $size_1$,则经过它的方案数为:


$$\sum_{i=0}^{m}\sum_{j=0}^{m}(\min(i, m-j)+\min(m-i, j))\times C^i_mC^j_m\times size_0^i\cdot size_1^{m-i}\cdot size_0^j\cdot size_1^{m-j}$$


直接计算的复杂度为 $\Theta(nm^2)$。

Subtask #3:$n,m\le2500\ 最多只有一条边的权值为正\ (20\ pts)$

对于权值为 $0$ 的边跳过即可。结合前述算法可得 $70\ pts$。

#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 5005;
const int maxm = 10005;
const ll P = 1e9 + 7;
int n, m, sz[maxn];
int tot, ter[maxm], wei[maxm], nxt[maxm], lnk[maxn];
ll ans, power[maxn][maxn], inv[maxn], fact[maxn], finv[maxn];
ll C(int n, int m) {
    return fact[n] * finv[m] % P * finv[n - m] % P;
}
void addedge(int u, int v, int w) {
    ter[++tot] = v;
    wei[tot] = w;
    nxt[tot] = lnk[u];
    lnk[u] = tot;
}
void dfs(int u, int p = 0) {
    sz[u] = 1;
    for (int v, w, e = lnk[u]; e; e = nxt[e]) {
        v = ter[e], w = wei[e];
        if (v == p) continue;
        dfs(v, u);
        sz[u] += sz[v];
        if (w == 0) continue;
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= m; j++) {
                ll x = w * (min(i, m - j) + min(m - i, j)) % P;
                x = x * C(m, i) % P * C(m, j) % P;
                x = x * power[sz[v]][i] % P * power[n - sz[v]][m - i] % P;
                x = x * power[sz[v]][j] % P * power[n - sz[v]][m - j] % P;
                ans = (ans + x) % P; 
            }
        }
    }
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
        power[i][0] = 1;
        for (int j = 1; j <= m; j++) {
            power[i][j] = power[i][j - 1] * i % P;
        }
    }
    inv[1] = 1;
    for (int i = 2; i <= m; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
    }
    fact[0] = finv[0] = 1;
    for (int i = 1; i <= m; i++) {
        fact[i] = fact[i - 1] * i % P;
        finv[i] = finv[i - 1] * inv[i] % P;
    }
    for (int u, v, w, i = 1; i < n; i++) {
        scanf("%d %d %d", &u, &v, &w);
        addedge(u, v, w);
        addedge(v, u, w);
    }
    dfs(1);
    printf("%lld\n", ans);
    return 0;
}
Subtask #4:$n,m\le2500\ (100\ pts)$

我们发现可以预处理出:


$$\sum_{i+j=m}(\min(i, m-j)+\min(m-i, j))\times C^i_mC^j_m$$


发现剩下的式子只和 $i+j$ 有关,然后对于每条边就可以 $\Theta(m)$ 计算了。

#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 5005;
const int maxm = 10005;
const ll P = 1e9 + 7;
int n, m, sz[maxn];
int tot, ter[maxm], wei[maxm], nxt[maxm], lnk[maxn];
ll ans, power[maxn][maxm], inv[maxn], fact[maxn], finv[maxn], sum[maxm];
ll C(int n, int m) {
    return fact[n] * finv[m] % P * finv[n - m] % P;
}
void addedge(int u, int v, int w) {
    ter[++tot] = v;
    wei[tot] = w;
    nxt[tot] = lnk[u];
    lnk[u] = tot;
}
void dfs(int u, int p = 0) {
    sz[u] = 1;
    for (int v, w, e = lnk[u]; e; e = nxt[e]) {
        v = ter[e], w = wei[e];
        if (v == p) continue;
        dfs(v, u);
        sz[u] += sz[v];
        if (w == 0) continue;
        for (int i = 0; i <= 2 * m; i++) {
            ll x = w * sum[i] % P;
            x = x * power[sz[v]][i] % P * power[n - sz[v]][2 * m - i] % P;
            ans = (ans + x) % P;
        }
    }
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
        power[i][0] = 1;
        for (int j = 1; j <= 2 * m; j++) {
            power[i][j] = power[i][j - 1] * i % P;
        }
    }
    inv[1] = 1;
    for (int i = 2; i <= m; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
    }
    fact[0] = finv[0] = 1;
    for (int i = 1; i <= m; i++) {
        fact[i] = fact[i - 1] * i % P;
        finv[i] = finv[i - 1] * inv[i] % P;
    }
    for (int u, v, w, i = 1; i < n; i++) {
        scanf("%d %d %d", &u, &v, &w);
        addedge(u, v, w);
        addedge(v, u, w);
    }
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= m; j++) {
            sum[i + j] = (sum[i + j] + (min(i, m - j) + min(m - i, j)) * C(m, i) % P * C(m, j) % P) % P;
        }
    }
    dfs(1);
    printf("%lld\n", ans);
    return 0;
}

Task #3 导数卷积 Convolution

Subtask #1:$n\le200\ (20\ pts)$

直接暴力计算即可。时间复杂度 $\Theta(n^3)$。

Subtask #2:$n\le1000\ (40\ pts)$

直接暴力计算 $f^{(i)}$ ,然后 NTT 计算卷积即可。时间复杂度 $\Theta(n^2\log_2n)$。

#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 1 << 13 | 1;
const ll P = 998244353, g = 3;
int n, m, k, r[maxn];
ll a[maxn][maxn], b[maxn], c[maxn];
ll mpow(ll a, ll b) {
    ll c = 1;
    b = (b % (P - 1) + P - 1) % (P - 1);
    for (; b; b >>= 1, a = a * a % P) {
        if (b & 1) {
            c = a * c % P;
        }
    }
    return c;
}
void ntt(ll *a, int n, int type) {
    for (int i = 0; i < n; i++) {
        if (i < r[i]) {
            swap(a[i], a[r[i]]);
        }
    }
    for (int k = 1; k < n; k <<= 1) {
        ll x = mpow(g, type * (P - 1) / (k << 1));
        for (int i = 0; i < n; i += k << 1) {
            ll y = 1;
            for (int j = i; j < i + k; j++, y = x * y % P) {
                ll p = a[j], q = y * a[j + k] % P;
                a[j] = (p + q) % P, a[j + k] = (p - q + P) % P;
            }
        }
    }
    if (type == -1) {
        ll x = mpow(n, P - 2);
        for (int i = 0; i < n; i++) {
            a[i] = x * a[i] % P;
        }
    }
}
int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%lld", &a[0][i]);
    }
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < n - 1; j++) {
            a[i][j] = 1ll * (j + 1) * a[i - 1][j + 1] % P;
        }
    }
    for (m = 1; m < n; m <<= 1) k++;
    for (int i = 0; i < m; i++) {
        r[i] = (r[i >> 1] >> 1) | ((i & 1) << (k - 1));
    }
    for (int i = 0; i < n; i++) {
        ntt(a[i], m, 1);
    }
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            b[j] = a[i][j] * a[n - i - 1][j] % P;
        }
        ntt(b, m, -1);
        for (int j = 0; j < n; j++) {
            c[j] = (b[j] + c[j]) % P;
        }
    }
    for (int i = 0; i < n; i++) {
        printf("%lld%c", c[i], " \n"[i == n - 1]);
    }
    return 0;
}
Subtask #3:$n\le5000\ (60\ pts)$

令 $F_i$ 为 $f(x)$ 第 $i$ 项的系数。则有:


$$G_d=\sum_{i=0}^{n-1}\sum_{0}^{d}F_{i+j}\cdot\dfrac{(i+j)!}{j!}\times F_{n-1+d-i-j}\cdot\dfrac{(n-1+d-i-j)!}{(d-j)!}$$


令 $H_i=I!\cdot F_i$,原式变为:


$$G_d=\sum_{i=0}^{n-1}\sum_{j=0}^dH_{i+j}\cdot\dfrac{1}{j!}\times H_{n-1+d-i-j}\cdot\dfrac{1}{(d-j)!}$$ $$G_d=\sum_{i=0}^{n-1+d}H_i\times H_{n-1+d-i}\cdot\sum_{0}^{d}\dfrac{1}{(d-j)!j!}$$ $$G_d=\dfrac{2^d}{d!}\cdot\sum_{i=0}^{n-1+d}H_i\times H_{n-1+d-i}$$


直接计算的复杂度为 $\Theta(n^2)$。

Subtask #4:$n\le100000\ (100\ pts)$

使用 NTT 计算上式,即可将复杂度优化到 $\Theta(n\log_2n)$。

#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 100005;
const int maxm = 1 << 18 | 1;
const ll P = 998244353, g = 3;
int n, m, b, r[maxm];
ll inv[maxn], finv[maxn];
ll fact[maxn], pow2[maxn];
ll A[maxm], B[maxm];
ll power(ll a, ll b) {
    ll c = 1;
    b = (b % (P - 1) + P - 1) % (P - 1);
    for (; b; b >>= 1, a = a * a % P) {
        if (b & 1) {
            c = a * c % P;
        }
    }
    return c;
}
void prework() {
    inv[1] = 1;
    for (int i = 2; i < n; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
    }
    fact[0] = finv[0] = 1;
    for (int i = 1; i < n; i++) {
        fact[i] = fact[i - 1] * i % P;
        finv[i] = finv[i - 1] * inv[i] % P;
    }
    pow2[0] = 1;
    for (int i = 1; i < n; i++) {
        pow2[i] = pow2[i - 1] * 2 % P;
    }
    for (m = 1; m < n << 1; m <<= 1) b++;
    for (int i = 0; i < m; i++) {
        r[i] = (r[i >> 1] >> 1) | ((i & 1) << (b - 1));
    }
}
void ntt(ll *a, int n, int type) {
    for (int i = 0; i < n; i++) {
        if (i < r[i]) {
            swap(a[i], a[r[i]]);
        }
    }
    for (int k = 1; k < n; k <<= 1) {
        ll x = power(g, type * (P - 1) / (k << 1));
        for (int i = 0; i < n; i += k << 1) {
            ll y = 1;
            for (int j = i; j < i + k; j++, y = x * y % P) {
                ll p = a[j], q = y * a[j + k] % P;
                a[j] = (p + q) % P, a[j + k] = (p - q + P) % P;
            }
        }
    }
    if (type == -1) {
        ll x = power(n, P - 2);
        for (int i = 0; i < n; i++) {
            a[i] = a[i] * x % P;
        }
    }
}
int main() {
    scanf("%d", &n);
    prework();
    for (int i = 0; i < n; i++) {
        scanf("%lld", &A[i]);
        A[i] = B[i] = A[i] * fact[i] % P;
    }
    ntt(A, m, 1), ntt(B, m, 1);
    for (int i = 0; i < m; i++) {
        A[i] = A[i] * B[i] % P;
    }
    ntt(A, m, -1);
    for (int i = 0; i < n; i++) {
        A[i + n - 1] = A[i + n - 1] * finv[i] % P * pow2[i] % P;
    }
    for (int i = n - 1; i < 2 * n - 1; i++) {
        printf("%lld%c", A[i], " \n"[i == 2 * n - 2]);
    }
    return 0;
}
gaolinxiang Avatar