2021"MINIEYE杯"中超(4)补题

2021"MINIEYE杯"中超(4)

1002 Kanade Loves Maze Designing

本题点数n最多为2000,因此允许O(n²)的做法,我们用dfs来计算任意两点间的点的种类数即可得出答案。因本题数据量较小,所以无需快速幂优化,直接循环n次预处理出K ^ (j - 1)即可。

#include <bits/stdc++.h>

using namespace std;

const int N = 2010, MOD1 = 1e9 + 7, MOD2 = 1e9 + 9, K = 19560929;

int t;
int n;
int cnt[N];
int am[N][N];
int c[N];
int ans;
int md1[N], md2[N];
vector<int> v[N];

void cal1(int i, int &k)//计算答案
{
    for (int j = 1; j <= n; j ++ ) k = (1ll * k + 1ll * am[i][j] * md1[j - 1]) % MOD1;
}

void cal2(int i, int &k)//计算答案
{
    for (int j = 1; j <= n; j ++ ) k = (1ll * k + 1ll * am[i][j] * md2[j - 1]) % MOD2;
}

void init()//预处理数组
{
    md1[0] = md2[0] = 1;
    for (int i = 1; i < N; i ++ )
    {
        md1[i] = 1ll * md1[i - 1] * K % MOD1;
        md2[i] = 1ll * md2[i - 1] * K % MOD2; 
    }
}
//+-1法求[i, j]间点的种类数
void add(int k)//加操作
{
    if (!cnt[k]) ans ++ ;
    cnt[k] ++ ;
    return;
}

void del(int k)//遍历过后的删除操作
{
    if (cnt[k] == 1) ans -- ;
    cnt[k] -- ;
    return;
}

void dfs(int now, int to, int fr)//删除操作,now表示从now点开始搜,to表示当前点,fr表示来向防止重复搜
{
    add(c[to]);//加上此点的值
    am[now][to] = ans;//记录now到此点的结果
    for (auto t : v[to])
        if (t != fr)//防止重复搜索
            dfs(now, t, to);
    del(c[to]);
    
    return;
}

void solve()
{
    cin >> n;
    for (int i = 2, x; i <= n; i ++ )
    {
        cin >> x;
        v[i].push_back(x), v[x].push_back(i);
    }
    for (int i = 1; i <= n; i ++ ) cin >> c[i];
    
    for (int i = 1; i <= n; i ++ )
    {
        dfs(i, i, -1);//当前搜i点,因为i点是起点,所以来向标记为-1
        
        int ans1 = 0, ans2 = 0;
        cal1(i, ans1), cal2(i, ans2);
        
        cout << ans1 << ' ' << ans2 << endl;
    }
    
    //for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) cout << am[i][j] << (j == n ? '\n' : ' ');
    for (int i = 1; i <= n; i ++ ) v[i].clear();
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    init();//初始化
    cin >> t;
    while (t -- ) solve();
    
    return 0;
}
1007 Increasing Subsequence

首先思考朴素一点的做法,采用一般处理线性dp的思路。f[i]表示以a[i]结尾的极大子序列,接着考虑贡献,对于j (j < i),只有[j, i]中间没有a[j] ~ a[i]的元素,那么f[j] 才对 f[i]有贡献。f[i]对答案有贡献当且仅当[i ~ n]中没有比a[i]大的元素,所以朴素一点的线性dp解法就有了。O(n²)的复杂度,考虑到n是1e5的级别,会t。

接着思考优化,处理区间[l, r]时,以m为终点将其分开,然后两边维护两个单调栈,左边单调栈元素值从小到大,下标从大到小,右边单调栈元素值从小到大,下标从小到大。接着考虑贡献,对于f[i] (i > mid),我们找到左边第一个比f[i]小的值a[j],因为右边也维护了一个单调栈,所以栈顶元素即满足要求,如果栈空那么就返回0。然后去找左边单调栈中第一个比a[j]大的值和最后一个比a[i]小的值,那么这个范围内都是对f[i]有贡献的。直接将这些dp值加上即可。为了方便计算,我们可以用一个vector储存左边区间的dp值前缀和,并且在维护单调栈的同时也维护这个前缀和dp的vector。

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

using namespace std;

typedef pair<int, int> pii;

const int P = 998244353;

int add(int a, int b) {a += b; return a < P ? a : a - P;}
int sub(int a, int b) {a -= b; return a < 0 ? a + P : a;}

const int N = 100001;

int a[N], dp[N];

void solve(int l, int r)
{
    if (l + 1 == r) return;
    
    int mid = (l + r) >> 1;
    solve(l, mid);//处理左边
    vector<int> pos(r - l);
    iota(pos.begin(), pos.end(), l);//递增赋值下标
    sort(pos.begin(), pos.end(), [&](int i, int j) { return a[i] < a[j]; });//按照值递增排序排序
    vector<int> ls, rs;
    vector<int> sum(1, 0);//初始化加入一个0,方便前缀和
    for (int i : pos)//pos是有序的,所以每个a[i]一定是比当前在栈中的值都要大
    {
        if (i < mid)
        {
            while (!ls.empty() && ls.back() < i)//在我左边而且值还比我小,一定没有贡献,删掉就行
                sum.pop_back(), ls.pop_back();
            ls.push_back(i);//将i加入左边序列的单调栈
            sum.push_back(add(sum.back(), dp[i]));//更新左边dp前缀和         
        }
        else 
        {
            while (!rs.empty() && rs.back() > i)//在我右边还比我小,一定没有贡献,同样删掉
                rs.pop_back();
            if (ls.empty()) continue;//如果左边是空的,没有可以继承过来的状态
            //id1是找到大于a[i]的第一个元素,由于dp前缀和中有一个0,所以偏移一下会到小于a[i]的最后一个元素
            int id1 = partition_point(ls.begin(), ls.end(), [&](int x) { return a[x] < a[i]; }) - ls.begin();
            //id2是找到大于a[rs.back()]的第一个元素
            int id2 = rs.empty() ? 0 : partition_point(ls.begin(), ls.end(), [&](int x) { return a[x] < a[rs.back()]; }) - ls.begin();
            //更新处于右边的dp数组
            dp[i] = add(dp[i], sub(sum[id1], sum[id2]));
            rs.push_back(i);
        }
    }
    solve(mid, r);//递归处理右边
}

int main(void) 
{
    int T; scanf("%d", &T);
    while (T--) 
    {
        int n; scanf("%d", &n);
        for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
        for (int i = 1, v = INT_MAX; i <= n; ++i)//处理dp数组
        {
            if (v > a[i])//左边最小的比当前的大,说明以a[i]结尾的有贡献
                dp[i] = 1;
            else //反之一定会成为其他序列的子序列,没有贡献
                dp[i] = 0;
            v = min(v, a[i]);
        }
        solve(1, n + 1);//左闭右开
        int ans = 0;
        for (int i = n, v = 0; i >= 1; --i)
        {
            if (v < a[i])
                ans = add(ans, dp[i]);
            v = max(v, a[i]);
        }
        printf("%d\n", ans);
    }
    return 0;
}
1008 Lawn of the Dead

对于每个点,只有其左边或者上面可以到达,它才可以到达。对于每个地雷,那么它一定挡住了从左边到达的这条路,这时候我们只能考虑从上方到达,因此对于每个地雷[i, j],我们考虑[i - 1, j + 1]这个点向右连续不可到达的区域,那么第i行的这些列的可达情况肯定和其是一致的。因此我们可以通过这种操作算出不可到达的数量,最后用总点数量减去不可到达的数量再减去地雷数得到的就是答案。涉及到区间查询和区间修改的操作,因此可以用线段树维护状态,另外,因为考虑第i行的时候,我们只需要考虑第i - 1行的状态,类比滚动数组的思想,我们只需要建两棵线段树即可完成上述操作。

code
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 200010;

int n, m, k;
int t;
vector<int> mine[N];//存每行地雷下标
struct node
{
    int p, l, r, val, lmx;//lmx为区间左端点开始连续不可到达的长度
    int lazy;//懒标记
}tr[2][N * 4];//两棵树

void build(int x, int u, int l, int r)
{
    tr[x][u].l = l, tr[x][u].r = r, tr[x][u].lazy = -1;//初始化懒标记
    if (l == r)
    {
        tr[x][u].val = tr[x][u].lmx = 0;
        tr[x][u].lazy = -1;
        
        return;
    }
    
    int mid = l + r >> 1;
    build(x, u << 1, l, mid), build(x, u << 1 | 1, mid + 1, r);
    
    //pushup操作
    tr[x][u].val = tr[x][u << 1].val + tr[x][u << 1 | 1].val;
    tr[x][u].lmx = 0;
}

void pushdown(int x, int u)
{
    if (tr[x][u].lazy != -1)//打标记了
    {
        //处理左子树
        tr[x][u << 1].val = (tr[x][u << 1].r - tr[x][u << 1].l + 1) * tr[x][u].lazy;
        tr[x][u << 1].lmx = (tr[x][u << 1].r - tr[x][u << 1].l + 1) * tr[x][u].lazy;
        //处理右子树
        tr[x][u << 1 | 1].val = (tr[x][u << 1 | 1].r - tr[x][u << 1 | 1].l + 1) * tr[x][u].lazy;
        tr[x][u << 1 | 1].lmx = (tr[x][u << 1 | 1].r - tr[x][u << 1 | 1].l + 1) * tr[x][u].lazy;
        //传递懒标记
        tr[x][u << 1].lazy = tr[x][u].lazy;
        tr[x][u << 1 | 1].lazy = tr[x][u].lazy;
        //清除父节点懒标记
        tr[x][u].lazy = -1;
    }
}

void modify(int x, int u, int l, int r, int v)
{
    if (tr[x][u].l >= l && tr[x][u].r <= r)
    {
        tr[x][u].val = (tr[x][u].r - tr[x][u].l + 1) * v;
        tr[x][u].lmx = (tr[x][u].r - tr[x][u].l + 1) * v;
        tr[x][u].lazy = v;
        
        return;
    }
    
    pushdown(x, u);
    int mid = tr[x][u].l + tr[x][u].r >> 1;
    if (l <= mid) modify(x, u << 1, l, r, v);
    if (r > mid) modify(x, u << 1 | 1, l, r, v);
    //pushup操作
    tr[x][u].val = tr[x][u << 1].val + tr[x][u << 1 | 1].val;//维护val即区间不可到达的数量
    if (tr[x][u << 1].val == tr[x][u << 1].r - tr[x][u << 1].l + 1)//左子树区间全部不可到达
        tr[x][u].lmx = tr[x][u << 1].val + tr[x][u << 1 | 1].lmx;//那么等于左区间尺寸加右区间不可达
    else tr[x][u].lmx = tr[x][u << 1].lmx;//否则就直接等于左区间不可达
    
    return;
}

int query(int x, int u, int l, int r)
{
    if (tr[x][u].l == l && tr[x][u].r == r) return tr[x][u].lmx;//此区间完全包含查询区间
    
    pushdown(x, u);
    int mid = tr[x][u].l + tr[x][u].r >> 1;
    if (r <= mid) return query(x, u << 1, l, r);
    if (l > mid)  return query(x, u << 1 | 1, l, r);
    
    int temp = query(x, u << 1, l, mid);//查询左子树,左边全部到不了再查右子树
    if (temp == mid - l + 1) return temp + query(x, u << 1 | 1, mid + 1, r);
    else return temp;
}

int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin >> t;
    while (t -- )
    {
        cin >> n >> m >> k;
        build(0, 1, 1, m), build(1, 1, 1, m);//建立两棵线段树
        for (int i = 0; i <= n + 10; i ++ ) mine[i].clear();//清空vector
        int x, y;
        for (int i = 1; i <= k; i ++ )
        {
            cin >> x >> y;
            mine[x].push_back(y);
        }    
        
        long long ans = 0;//储存不可到达的非地雷区域数目
        modify(0, 1, 1, m, 1);//处理第0行,懒标记为1表示不可到达
        for (int i = 1; i <= n; i ++ )//一行一行处理
        {
            sort(mine[i].begin(), mine[i].end());//将地雷按照纵坐标升序排列
            mine[i].push_back(m + 1);//保证最后一个区间一定有右端点,加一个哨兵
            int last = 0;//此行上一个雷的位置
            modify((i & 1), 1, 1, m, 0);//清空线段树
            
            for (int j = 0; j < mine[i].size(); j ++ )//遍历所有雷
            {
                int poi = mine[i][j];//雷的位置
                if (i == 1 && !last) {last = poi; continue;}//处理第一行
                int l = last + 1, r = poi - 1, len;//处理区间[l + 1, r - 1]
                if (l <= r)
                {
                    len = query((i & 1) ^ 1, 1, l, r);//查询上一行
                    ans += 1ll * len;
                }
                else len = 0;//否则此区间不存在不可到达的点
                if (!(l - 1 == 0 && l - 1 + len == 0)) modify((i & 1), 1, l - 1, l - 1 + len, 1);//标记左侧的地雷
                last = poi;//更新上一个地雷的位置
            }
        }
        
        cout << 1ll * n * m - ans - 1ll * k << endl;
    }
    
    return 0;
}
posted @ 2021-08-05 01:40  sunnyday0725  阅读(25)  评论(0编辑  收藏  举报