noip2021训练2(CF)

Link

CF1100E Andrew and Taxi

Description

给定一个有向图,改变其中某些边的方向,它将成为一个有向无环图。

现在求一个改变边方向的方案,使得所选边边权的最大值最小。

Solution

使得最大值最小,很明显二分答案。

对于二分的答案 \(x\),考虑如何判断是否为有向无环图,也就是判断是否有环,可以 \(dfs\) 也可以 \(bfs\),但是因为要构造一组解,所以这里使用拓扑排序。

只要能通过边权 \(>x\) 的边遍历每个点,就说明无环,同时将拓扑序记下来,用来求要改变哪些边。

在跑完拓扑后,将边权 \(\le x\) 的边枚举一遍,如果 \(v\) 的拓扑序小于 \(u\),也就是一条返祖边,就会产生环,需要改变方向。

Code
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;
int n, m, w[N], cnt;
struct edge
{
    int v, w, nxt, id;
}e[N];
int head[N], tot;

void add(int u, int v, int w, int id)
{
    e[++tot] = (edge){v, w, head[u], id};
    head[u] = tot;
}

queue <int> que;
int deg[N], tim[N], id[N];
bool vis[N];

bool chk(int x)
{
    memset(deg, 0, sizeof(deg));
    memset(vis, 0, sizeof(vis));
    memset(tim, 0, sizeof(tim));
    while(!que.empty()) que.pop();
    for(int i = 1; i <= n; i++)
        for(int j = head[i]; j; j = e[j].nxt)
            if(e[j].w > x) deg[e[j].v]++;
    for(int i = 1; i <= n; i++)
        if(!deg[i]) que.push(i);

    int num = 0;
    while(!que.empty())
    {
        int u = que.front();
        que.pop();
        tim[u] = ++num;
        for(int i = head[u]; i; i = e[i].nxt)
        {
            if(e[i].w <= x) continue;
            int v = e[i].v;
            if((--deg[v]) == 0) que.push(v);
        }
    }
    if(num != n) return false;

    cnt = 0;
    for(int i = 1; i <= n; i++)
        for(int j = head[i]; j; j = e[j].nxt)
        {
            if(e[j].w > x) continue;
            int v = e[j].v;
            if(tim[v] < tim[i]) id[++cnt] = e[j].id;
        }
    return true;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1, u, v; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w[i]);
        add(u, v, w[i], i);
    }
    sort(w + 1, w + 1 + m);

    int l = 0, r = m, ans;
    while(l <= r)
    {
        int mid = (l + r) >> 1;
        if(chk(w[mid])) r = mid - 1, ans = w[mid];
        else l = mid + 1;
    }

    printf("%d %d\n", ans, cnt);
    sort(id + 1, id + 1 + cnt);
    for(int i = 1; i <= cnt; i++)
        printf("%d ", id[i]);
    return 0;
}

CF632F Magic Matrix

Description

定义一个大小为 \(n\times n\) 矩阵 \(a\) 为魔法矩阵,当且仅当 \(a\) 满足以下条件:

以下 \(i,j,k\in \mathbb{Z^+}\)

  • \(\forall i\in[1,n],a_{i,i}=0\)

  • \(\forall i,j\in[1,n],a_{i,j}=a_{j,i}\)

  • \(\forall i,j,k\in[1,n],a_{i,j}\le\max(a_{i,k},a_{j,k})\)

给你一个矩阵,问它是不是魔法矩阵。 如果 \(a\) 是魔法矩阵输出 \(\texttt{MAGIC}\),否则输出 \(\texttt{NOT MAGIC}\)

\(1\leq n\leq 2500,\forall i,j\in[1,n],0\le a_{i,j}\le 10^9\)

第三条也就是对于任意 \(a_{i,j}\),在第 \(i\) 行和第 \(j\) 行不存在同一列上的值都小于 \(a_{i,j}\)

Solution

第一条可以 \(O(n^2)\) 判断。

第二条可以 \(O(n)\) 判断。

考虑第三条

暴力的做法,对于 \(a_{i,j}\),枚举 \(k\) 判断 \(a_{i,j} \le max(a_{i,k},a_{j,k})\)

但是这样是 \(O(n^3)\) 的,考虑优化

可以将每个点取出来,从小到大排序,然后一个一个模拟往回加,这样在当前数以前加进去的都是小于当前数的。

对于每行用 \(bitset\) 维护一下,初值为 \(0\),若某个值为 \(1\),说明是在前面加进去的,也就是小于当前数,判断时只需要将两行按位与,只要没有 \(1\) 就是合法的,然后将 \((i,j)\) 改为 \(1\)

注意对于值相同的数需要一起处理。

Code
#include <bits/stdc++.h>

using namespace std;

const int N = 2510;
int n, a[N][N];
struct node
{
    int x, y, val;
    friend bool operator < (node a, node b)
    {
        return a.val < b.val;
    }
}p[N * N];
int cnt;
bitset <N> f[N];

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
        {
            scanf("%d", &a[i][j]);
            p[++cnt] = (node) {i, j, a[i][j]};
        }
    sort(p + 1, p + 1 + cnt);

    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(a[i][j] != a[j][i] || a[i][i])
            {
                printf("NOT MAGIC\n");
                return 0;
            }
            
    for(int i = 1, j; i <= cnt; i = j + 1)
    {
        j = i;
        while(j <= cnt && p[j + 1].val == p[i].val) j++;
        for(int k = i; k <= j; k++)
            if((f[p[k].x] & f[p[k].y]).any())
            {
                printf("NOT MAGIC\n");
                return 0;
            }
        for(int k = i; k <= j; k++)
            f[p[k].x][p[k].y] = 1;
    }
    printf("MAGIC\n");
    return 0;
}

CF1131F Asya And Kittens

Description

有一个长为 \(n\) 的排列,一开始每个数都是一个独立的联通块。有 \(n−1\) 次操作,每次要求 \(x_i\)\(y_i\) 所在的联通块相邻,然后把这两个联通块合并。求一个合法的排列使得所有操作合法。保证有解。

\(1 \le n \le 1.5\times 10^5\)

Solution

对于维护连通性的题,考虑并查集,但是要构造一个排列,该怎么办呢?

我们发现,对于每个操作,只需要两个联通块相邻即可,所以我们只需要维护每个联通快内的点,在合并时,将一个联通块内的点复制到另一个联通块后面就行了。

一开始每个联通块就是它自己。

为了保证时间复杂度,需要启发式合并,就是判一下两个联通块哪个小,将小的接在大的下面。

Code
#include <bits/stdc++.h>

using namespace std;

const int N = 15e4 + 5;
int n, f[N];
vector <int> vec[N];
int rt;

int Find(int a)
{
    return f[a] == a ? a : f[a] = Find(f[a]);
}

void Union(int x, int y)
{
    x = Find(x), y = Find(y);
    if(x == y) return;
    if(vec[x].size() > vec[y].size())
    {
        f[y] = x;
        for(auto i : vec[y])
            vec[x].push_back(i);
    }
    else
    {
        f[x] = y;
        for(auto i : vec[x])
            vec[y].push_back(i);
    }
    rt = f[x];
    return;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        f[i] = i, vec[i].push_back(i);
    for(int i = 1, x, y; i < n; i++)
    {
        scanf("%d%d", &x, &y);
        Union(x, y);
    }
    for(auto i : vec[rt])
        printf("%d ", i);
    putchar('\n');
    return 0;
}

CF949C Data Center Maintenance

Description

给出 \(n\) 个数据中心,\(m\) 份资料。要把 \(m\) 份资料放到其中的两个数据中心备份,需要保证任意时刻都可以至少在一个数据中心进行备份。一天有 \(h\) 个小时,每个数据中心在一天内有一小时维护时间 \(u_i\)\(0 \leq u_i < h\)),在这一小时内该数据中心无法进行备份。

由于某种原因,需要把一些数据中心的维护时间向后推迟 1 小时(一个数据中心的维护时间的向后推迟可能导致有的资料无法在任意时刻进行备份 且 若推迟前 \(u_i = h - 1\) 那么推迟后 \(u_i = 0\)),请你求出最少需要向后推迟多少个数据中心,并把这些数据中心的编号输出出来。

\(2\le n \le 10^5,1\le m \le 10^5,2\le h \le 10^5\)

Solution

对于第 \(i\) 份资料,如果它的两个数据中心的维护时间相邻,那么在前面的推迟时,后面的也要推迟,就连一条从前面到后面边。

然后得到了一张图,不难发现推迟任意一个点后需要推迟的点的个数为这个点能到多少个点。

于是先 \(tarjan\) 缩点,得到一个 \(DAG\),显然选出度为 \(0\)\(scc\) 更优。

因此只需要枚举出度为 \(0\)\(scc\),取 \(scc\) 内点的个数的最小值即可。

Code
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;
int n, m, h, a[N];
vector <int> g[N];
int dfn[N], low[N], tim;
int stk[N], top, cnt[N], num, id[N];
bool vis[N];
int deg[N];

void tarjan(int u)
{
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    vis[u] = 1;
    for(auto v : g[u])
    {
        if(!dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
        else if(vis[v]) low[u] = min(low[u], dfn[v]);
    }
    if(dfn[u] == low[u])
    {
        int x; num++;
        do
        {
            x = stk[top--];
            id[x] = num;
            vis[x] = 0;
            cnt[num]++;
        } while (x != u);
    }
    return;
}

int main()
{
    scanf("%d%d%d", &n, &m, &h);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for(int i = 1, x, y; i <= m; i++)
    {
        scanf("%d%d", &x, &y);
        if((a[x] + 1) % h == a[y]) g[x].push_back(y);
        if((a[y] + 1) % h == a[x]) g[y].push_back(x);
    }

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

    for(int i = 1; i <= n; i++)
        for(auto j : g[i])
            if(id[i] != id[j])
                deg[id[i]]++;

    int ans = 1;
    for(int i = 1; i <= num; i++)
        if(!deg[i] && cnt[i] < cnt[ans]) ans = i;

    printf("%d\n", cnt[ans]);
    for(int i = 1; i <= n; i++)
        if(id[i] == ans) printf("%d ", i);
    putchar('\n');
    return 0;
}

CF343D Water Tree

Description

给定 \(n\) 个点的一棵有根树,\(1\) 号点是根,维护三种操作:

  1. 把结点 \(v\) 及其子树赋值 \(1\)
  2. 把结点 \(v\) 及其到根上的路径赋值 \(0\)
  3. 询问 \(v\) 点的值。

初始时所有的点都是 \(0\)

Solution

题如其名,非常的 \(water\)

树剖板子题。

Code
#include <bits/stdc++.h>

using namespace std;

const int N = 5e5 + 5;
int n, q;
vector <int> g[N];

int f[N], dep[N], siz[N], son[N];

void dfs1(int u, int fa)
{
    f[u] = fa;
    dep[u] = dep[fa] + 1;
    siz[u] = 1;
    for(auto v : g[u])
    {
        if(v == fa) continue;
        dfs1(v, u);
        siz[u] += siz[v];
        if(siz[v] > siz[son[u]]) son[u] = v;
    }
    return;
}

int top[N], id[N], cnt;

void dfs2(int u, int tp)
{
    top[u] = tp;
    id[u] = ++cnt;
    if(son[u]) dfs2(son[u], tp);
    for(auto v : g[u])
    {
        if(v == f[u] || v == son[u]) continue;
        dfs2(v, v);
    }
    return;
}

#define ls (rt << 1)
#define rs (rt << 1 | 1)

int val[N << 2], tag[N << 2];

void pushdown(int rt)
{
    if(tag[rt] != -1)
    {
        val[ls] = tag[rt];
        val[rs] = tag[rt];
        tag[ls] = tag[rt];
        tag[rs] = tag[rt];
        tag[rt] = -1;
    }
}

void upd(int L, int R, int v, int l, int r, int rt)
{
    if(l > R || r < L) return;
    if(L <= l && r <= R)
    {
        val[rt] = tag[rt] = v;
        return;
    }
    pushdown(rt);
    int mid = (l + r) >> 1;
    upd(L, R, v, l, mid, ls);
    upd(L, R, v, mid + 1, r, rs);
}

int qry(int pos, int l, int r, int rt)
{
    if(l == r) return val[rt];
    pushdown(rt);
    int mid = (l + r) >> 1;
    if(pos <= mid) return qry(pos, l, mid, ls);
    else return qry(pos, mid + 1, r, rs);
}

void update(int x)
{
    while(top[x] != 1)
    {
        upd(id[top[x]], id[x], 0, 1, n, 1);
        x = f[top[x]];
    }
    upd(1, id[x], 0, 1, n, 1);
    return;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1, u, v; i < n; i++)
    {
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs1(1, 0), dfs2(1, 1);
    memset(tag, -1, sizeof(tag));
    scanf("%d", &q);
    while(q--)
    {
        int op, x;
        scanf("%d%d", &op, &x);
        if(op == 1) upd(id[x], id[x] + siz[x] - 1, 1, 1, n, 1);
        else if(op == 2) update(x);
        else printf("%d\n", qry(id[x], 1, n, 1));
    }
    return 0;
}

CF85E Guard Towers

Description

已知 \(n\) 座塔的坐标,把他们分成两组,使得同组内的两座塔的曼哈顿距离的最大值最小。

在此前提下求出有多少种分组方案 \(\mod10^9+7\)

\(n\le 5000\)

Solution

又是最大值最小,直接二分答案。

观察到是分成两组,并且组内边权 \(\le x\),两组间边权 \(>x\),比较容易想到二分图。

对于二分的答案 \(x\),只需要将 \(>x\) 的边连起来,进行01染色,判断是否为二分图。

考虑如何计算方案数,图不一定联通,对于每个联通块,是可以交换两边点的位置的,有两种方案,因此总方案数是 \(2^{cnt}\)\(cnt\) 为联通块的个数。

那么联通块个数怎么求,其实就是在枚举每个点判断如果没被染色就进行01染色时,起点的个数。

Code
#include <bits/stdc++.h>
#define ll long long

using namespace std;

const int N = 5010;
const int p = 1e9 + 7;
int n, x[N], y[N];
int col[N];
int ans, cnt;

int Dis(int i, int j)
{
    return abs(x[i] - x[j]) + abs(y[i] - y[j]);
}

bool dfs(int u, int x)
{
    for(int v = 1; v <= n; v++)
        if(Dis(u, v) > x)
        {
            if(col[v] == -1)
            {
                col[v] = col[u] ^ 1;
                if(!dfs(v, x)) return false;
            }
            else if(col[v] == col[u]) return false;
        }
    return true;
}

bool chk(int x)
{
    memset(col, -1, sizeof(col));
    int num = 0;
    for(int i = 1; i <= n; i++)
        if(col[i] == -1)
        {
            num++, col[i] = 0;
            if(!dfs(i, x)) return false;
        }
    cnt = num;
    return true;
}

ll qpow(ll a, int b)
{
    ll res = 1;
    while(b)
    {
        if(b & 1) res = res * a % p;
        a = a * a % p, b >>= 1;
    }
    return res;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d%d", &x[i], &y[i]);

    int l = 0, r = 1e4;
    while(l <= r)
    {
        int mid = (l + r) >> 1;
        if(chk(mid)) r = mid - 1, ans = mid;
        else l = mid + 1;
    }

    printf("%d\n%lld\n", ans, qpow(2, cnt));
    return 0;
}

CF711D Directed Roads

Description

\(n\) 个点和 \(n\) 条边,第 \(i\) 条边从 \(i\) 连到 \(a_i\) 。 每条边需要指定一个方向(可以将边反向)。问有多少种指定方向的方案使得图中不出现环。

\(2\le n \le 2\times 10^5,1\le a_i \le n,a_i \ne i\)

Solution

观察到 \(n\) 个点 \(n\) 条边,因此是个基环树森林。

设环上的边数为 \(tot\),第 \(i\) 个环上的边数为 \(cnt_i\)

对于环上的边,第 \(i\) 个环上的方案数为 \(2^{cnt_i}\),但是有两种情况是环,因此是 \(\prod (2^{cnt_i}-2)\)

对于不是环上的边,可以任意指定方向,方案数为 \(2^{n-tot}\)

所以答案即为上面两种情况之积。

Code
#include <bits/stdc++.h>
#define ll long long

using namespace std;

const int N = 2e5 + 5;
const int p = 1e9 + 7;
int n;
vector <int> g[N];
int dfn[N], low[N], tim;
int stk[N], top, cnt[N], num;
bool vis[N];

void tarjan(int u)
{
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    vis[u] = 1;
    for(auto v : g[u])
    {
        if(!dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
        else if(vis[v]) low[u] = min(low[u], dfn[v]);
    }
    if(dfn[u] == low[u])
    {
        int x; num++;
        do
        {
            x = stk[top--];
            vis[x] = 0;
            cnt[num]++;
        } while (x != u);
    }
    return;
}

ll qpow(ll a, int b)
{
    ll res = 1;
    while(b)
    {
        if(b & 1) res = res * a % p;
        a = a * a % p, b >>= 1;
    }
    return res;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1, a; i <= n; i++)
    {
        scanf("%d", &a);
        g[i].push_back(a);
    }

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

    int tot = 0;
    for(int i = 1; i <= num; i++)
        if(cnt[i] > 1) tot += cnt[i];
    
    ll ans = qpow(2, n - tot);
    for(int i = 1; i <= num; i++)
        if(cnt[i] > 1) ans = ans * (qpow(2, cnt[i]) - 2 + p) % p;
    printf("%lld\n", ans);
    return 0;
}

CF17E Palisection

Description

给定一个长度为 \(n\) 的小写字母串,问你有多少对相交的回文子串(包含也算相交)。

\(1\le n \le 2 \times 10^6\),将答案对 \(51123987\) 取模。

Soluiton

正难则反,用总数减去不相交的。

考虑不相交的怎么算,用 \(l_i\) 表示以 \(i\) 为左端点的回文串的个数,\(r_i\) 表示以 \(i\) 为右端点的回文串的个数。

对于每个 \(l_i\) ,不相交的回文串的个数为 \(l_i\times \sum_{j=1}^ir_j\),这样就不重不漏了。

但是 \(l_i,r_i\) 并不好求

先用 \(manacher\) 求出每个位置的最长回文半径,但是只有最长的,然而中间每个位置也都会 \(+1\),所以用差分的小技巧,修改边界,最后跑一遍前缀和,就得到了 \(l,r\) 数组。

总数就是在所有回文串中任选两个的方案数。

注意最后在减去不相交的时候只枚举原串,不枚举在中间插入的字符。

Code
#include <bits/stdc++.h>
#define ll long long

using namespace std;

const int N = 4e6 + 5;
const int P = 51123987;
int n;
char s[N];
ll p[N], l[N], r[N];

void read(char s[])
{
    n = 1;
    s[0] = '@', s[1] = '#';
    char c = getchar();
    while(c < 'a' || c > 'z') c = getchar();
    while(c >= 'a' && c <= 'z') s[++n] = c, s[++n] = '#', c = getchar();
    return;
}

void manacher()
{
    for(int i = 1, mid = 0, r = 0; i <= n; i++)
    {
        if(i < r) p[i] = min(p[mid * 2 - i], (ll)r - i + 1);
        while(s[i - p[i]] == s[i + p[i]]) p[i]++;
        if(i + p[i] - 1 > r) r = i + p[i] - 1, mid = i;
    }
    return;
}

int main()
{
    scanf("%d", &n);
    read(s);

    manacher();
    
    ll ans = 0;
    for(int i = 1; i <= n; i++)
    {
        ans = (ans + (p[i] >> 1)) % P;
        l[i - p[i] + 1]++, l[i + 1]--;
        r[i]++, r[i + p[i]]--;
    }
    for(int i = 1; i <= n; i++)
        l[i] += l[i - 1], r[i] += r[i - 1];
    ans = ans * (ans - 1) / 2 % P;

    ll sum = 0;
    for(int i = 2; i <= n; i += 2)
    {
        ans = (ans - l[i] * sum % P + P) % P;
        sum = (sum + r[i]) % P;
    }
    printf("%lld\n", ans);
    return 0;
}

CF40E Number Table

Description

有一个 \(n\)\(m\) 列的棋盘,现在棋盘上的每个位置需要填上 \(1\)\(-1\) 。 其中有 \(k\) 个位置已经填上了数,而其它的位置则是空的。 定义一个棋盘是好的当且仅当其每一行、每一列都满足所有数的乘积是 \(−1\) 。 你需要计算有多少种填数的方案是好的,答案对 \(p\) 取模。

\(1\le n,m \le 10^3,0\le k < max(n,m),2\le p\le 10^9+7\)

Solution

注意到 \(0\le k < max(n,m)\),我们假设 \(n>m\),如果 \(n<m\),就交换行列。

这样,不论 \(k\) 个数怎么放,都会空出来一行,而空行可以让每一列乘积都成为 \(-1\),就确定了,所以直接求前 \(n-1\) 行的方案数,也就不需要再考虑列了。

对于每一行,记一下已经填了几个数 \(cnt\),和当前的乘积 \(mul\),然后分类讨论:

  1. \(cnt=m,mul=1\),无解
  2. \(cnt=m,mul=-1\),直接跳过
  3. \(cnt=0\) 并且还没有选定空行,将这一行定为空行
  4. 否则,先去掉一个数用来将这一行的乘积调为 \(-1\),剩下的就可以任意了, \(ans \times 2^{m-cnt-1}\)
Code
#include <bits/stdc++.h>
#define sawp(a, b) a ^= b ^= a ^= b
#define ll long long

using namespace std;

const int N = 1010;
int n, m, k, p;
int cnt[N], mul[N];
ll pow2[N], ans = 1;
bool rev, flag;

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    
    if((n & 1) != (m & 1))
    {
        printf("0\n");
        return 0;
    }

    if(n < m) swap(n, m), rev = 1;
    for(int i = 1; i <= n; i++) mul[i] = 1;
    for(int i = 1, x, y, z; i <= k; i++)
    {
        scanf("%d%d%d", &x, &y, &z);
        if(rev) swap(x, y);
        cnt[x]++, mul[x] *= z;
    }
    scanf("%d", &p);

    pow2[0] = 1;
    for(int i = 1; i <= m; i++)
        pow2[i] = pow2[i - 1] * 2 % p;

    for(int i = 1; i <= n; i++)
    {
        if(cnt[i] == m && mul[i] == 1) {ans = 0; break;}
        else if(cnt[i] == m && mul[i] == -1) continue;
        else if(!cnt[i] && !flag) flag = 1;
        else ans = ans * pow2[m - cnt[i] - 1] % p;
    }
    printf("%lld\n", ans);
    return 0;
}
posted @ 2021-11-02 22:32  Acestar  阅读(57)  评论(0编辑  收藏  举报