大二寒假第一周学习笔记

大二的课业比大一重了许多,导致最后几个星期没有训练,去复习期末考试去了。

练起

周三

CF14D Two Paths(暴力枚举+树的直径)

首先要考虑最长的路径,可以想到树的直径

但是我自己想的时候一直卡在如何使两条路径不相交的问题

数据只有200,解决方法非常暴力,直接枚举删除哪一条边,这样就变成了两颗树,然后两棵树分别求树的直径相乘即可。

关键在于直径暴力枚举删去哪一条边,这样两条路径就不会相交

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 200 + 10;
vector<int> g[N];
vector<pair<int, int>> ve;
map<pair<int, int>, bool> mp;
int vis[N], d[N], n, p;

void dfs(int u, int fa)
{
    vis[u] = 1;
    for(int v: g[u])
        if(v != fa && !mp[make_pair(u, v)])
        {
            d[v] = d[u] + 1;
            if(d[v] > d[p]) p = v;
            dfs(v, u);
        }
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
        ve.push_back(make_pair(u, v));
    }

    int ans = 0;
    for(auto e: ve)
    {
        mp[make_pair(e.first, e.second)] = 1;
        mp[make_pair(e.second, e.first)] = 1;
        memset(vis, 0, sizeof(vis));

        int cur = 1;
        _for(i, 1, n)
            if(!vis[i])
            {
                p = 0;
                d[i] = 0; dfs(i, 0);
                d[p] = 0; dfs(p, 0);
                cur *= d[p];
            }
            
        ans = max(ans, cur);
        mp[make_pair(e.first, e.second)] = 0;
        mp[make_pair(e.second, e.first)] = 0;
    }
    printf("%d\n", ans);

	return 0;
}

高维前缀和

做题时遇到了这个知识点,做一做这个知识点相关的题目

[学习笔记]高维前缀和 - *Miracle* - 博客园

核心代码

    rep(i, 0, w)
        rep(j, 0, 1 << w)  
            if(j & (1 << i))
                f[j] += f[j ^ (1 << i)];

CF165E Compatible Numbers(高维前缀和)

对于一个询问,显然有1的地方都不能有1

所以也就是当前的a[i]取反,问有没有元素是它的子集

二进制,子集,可以用高维前缀和

用高维前缀和的思路,处理出每一个数的子集中存不存在题目给的数,存在的话用任意一个数就可以。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int f[1 << 22], a[N], w = 22, n;

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
    {
        scanf("%d", &a[i]);
        f[a[i]] = a[i];
    }

    rep(i, 0, w)
        rep(j, 0, 1 << w)
            if(j & (1 << i) && f[j ^ (1 << i)]) f[j] = f[j ^ (1 << i)];
    
    _for(i, 1, n)
    {
        int cur = ((1 << w) - 1) ^ a[i];
        printf("%d ", f[cur] ? f[cur] : -1);
    }

	return 0;
}

E - Or Plus Max(思维 + 子集dp)

原来高维前缀和其实就是子集dp

这道题在于思路非常巧妙

首先i or j <= k 可以转化成i or j = k 然后求前缀最大值

对于i or j = k 其实不好处理 再转化成i or j是k的子集

这样子依然可以保持答案正确 所以用子集dp处理出每一个二进制数的子集的最大值和次大值就好了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int dp[1 << 18][2], n;  // 1最大 2次大

int main()
{
    scanf("%d", &n);
    rep(i, 0, 1 << n) 
    {
        int x; scanf("%d", &x);
        dp[i][1] = x;
    }

    rep(i, 0, n)
        rep(j, 0, 1 << n)
            if(j & (1 << i))
            {
                vector<int> ve;
                ve.push_back(dp[j][1]); 
                ve.push_back(dp[j][0]);
                ve.push_back(dp[j ^ (1 << i)][1]); 
                ve.push_back(dp[j ^ (1 << i)][0]);
                sort(ve.begin(), ve.end(), greater<int>());
                dp[j][1] = ve[0];
                dp[j][0] = ve[1];
            }
    
    int ans = 0;
    rep(i, 1, 1 << n)
    {
        ans = max(ans, dp[i][1] + dp[i][0]);
        printf("%d\n", ans);
    }

	return 0;
}

F. Bits And Pieces(子集dp)

首先是求位运算的最大值,这种题目常见的思路是从高到低贪心,尽可能为1

可以枚举ai,然后从高到低贪心

如果ai的这一位为1,那么另外一边无所谓

如果ai的这一位为0,就看是否有下标大于i的两个数的这一位都为1

这个问题可以转化成超集的问题,非常巧妙

用子集dp处理出每个集合的所有超集中,下标次大的一个,如何这个大于i,就可以。

同时为了让这个影响持续下去,要用一个check变量,来存之前已经用过了哪些1,所以每次求的是check | 1 << i 的超集。

子集dp求超集的话就状态转移的时候反过来就可以了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int dp[1 << 22][2], a[N], n;    //1最大 0次大

void insert(int i, int val)
{
    if(val > dp[i][1])
    {
        dp[i][0] = dp[i][1];
        dp[i][1] = val;
    }
    else if(val > dp[i][0])
        dp[i][0] = val;
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) 
    {
        scanf("%d", &a[i]);
        insert(a[i], i);
    }

    rep(i, 0, 22)
        rep(j, 0, 1 << 22)
            if(j & (1 << i))
            {
                insert(j ^ (1 << i), dp[j][0]);
                insert(j ^ (1 << i), dp[j][1]);
            }

    int ans = 0;
    _for(i, 1, n - 2)
    {
        int check = 0;
        for(int j = 21; j >= 0; j--)
        {
            if(a[i] & (1 << j)) continue;
            if(dp[check | (1 << j)][0] > i)
                check |= 1 << j;
        }
        ans = max(ans, a[i] | check);
    }
    printf("%d\n", ans);

	return 0;
}

周四

昨天打了一场cf

今天补补题

A. Ancient Civilization

签到题

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 100 + 10;
int a[N];

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        int n, l, ans = 0;
        scanf("%d%d", &n, &l);
        _for(i, 1, n) scanf("%d", &a[i]);

        rep(j, 0, l)
        {
            int zero = 0, one = 0;
            _for(i, 1, n)
                if(a[i] & (1 << j)) one++;
                else zero++;
            if(one > zero) ans |= 1 << j;
        }

        printf("%d\n", ans);
    }

	return 0;
}

B. Elementary Particles

因为数组范围开错了RE了一次 主要要检查一下数组空间开够没有

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int a[N], pre[N], n;

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) scanf("%d", &a[i]);
        memset(pre, 0, sizeof pre);

        int ans = -1;
        _for(i, 1, n)
        {
            if(pre[a[i]])
                ans = max(ans, pre[a[i]] - 1 + 1 + n - i);
            pre[a[i]] = i;
        }
        printf("%d\n", ans);
    }

	return 0;
}

C. Road Optimization(dp)

比较常规的dp 500的数据量就是O(n^3)的数据范围

dp[i][j]表示跑完前i段,用了j次的最优解

一开始用的是末尾连续用了j次,WA了一发,发现这一不能限制总次数,应该状态是设为一共用了这么多次的情况

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 500 + 10;
int d[N], a[N], dp[N][N], n, l, k;

int main()
{
    scanf("%d%d%d", &n, &l, &k);
    _for(i, 1, n) scanf("%d", &d[i]);
    _for(i, 1, n) scanf("%d", &a[i]);
    d[n + 1] = l;

    memset(dp, 0x3f, sizeof dp);
    dp[0][0] = 0;
    _for(i, 1, n)
        _for(j, 0, k) //当前用j次
        {
            if(i - j - 1 < 0) break;
            _for(t, 0, k - j) //之前用t次
                dp[i][j + t] = min(dp[i][j + t], a[i - j] * (d[i + 1] - d[i - j]) + dp[i - j - 1][t]);
        }

    int ans = 1e9;
    _for(i, 0, k) ans = min(ans, dp[n][i]);
    printf("%d\n", ans);

	return 0;
}

CF449D Jzzhu and Numbers(子集dp)

这题好妙啊

首先,一堆数的或和为j,那么这些数一定为j的子集

一堆数与和为j,那么这些数一定为j的超集

这道题求的是与和为0的方案数

高维前缀和可以求前缀和,这里是求具体某一个值,那么就可以先求前缀和,然后差分回来得到每一个数的值

具体来说,先求j的超集的个数,假设为cnt

那么与和为j的超集的方案数就是2 ^ cnt - 1

注意这个是前缀和,那么我们再差分回来就好了

从一维来看,差分其实就是前缀和反过来

这里的高维前缀和也是一样,把加号改成减号就可以了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e9 + 7;
const int N = 1e6 + 10;
const int w = 20;
int dp[1 << w], n;

int binpow(int a, int b)
{
    int res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = 1LL * res * a % mod;
        a = 1LL * a * a % mod;
    }
    return res;
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        dp[x]++;
    }

    rep(i, 0, w)
        rep(j, 0, 1 << w)
            if(j & (1 << i))
                dp[j ^ (1 << i)] = (dp[j ^ (1 << i)] + dp[j]) % mod;

    rep(j, 0, 1 << w) dp[j] = binpow(2, dp[j]) - 1;

    rep(i, 0, w)
        rep(j, 0, 1 << w)
            if(j & (1 << i))
                dp[j ^ (1 << i)] = ((dp[j ^ (1 << i)] - dp[j]) % mod + mod) % mod;
    
    printf("%d\n", dp[0]);

	return 0;
}

P6442 [COCI2011-2012#6] KOŠARE(子集dp)

和上一题类似 买一送一

这道题就是反过来,求或和为全集的方案数

同样有多个子集,然后2 ^ cnt -1求方案数 然后再差分

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e9 + 7;
const int w = 20;
int dp[1 << w], n, m;

int binpow(int a, int b)
{
    int res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = 1LL * res * a % mod;
        a = 1LL * a * a % mod;
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, n)
    {
        int k, cur = 0; 
        scanf("%d", &k);
        while(k--)
        {
            int x; scanf("%d", &x);
            cur |= 1 << (x - 1);
        }
        dp[cur]++;
    }

    rep(i, 0, m)
        rep(j, 0, 1 << m)
            if(j & (1 << i))
                dp[j] = (dp[j] + dp[j ^ (1 << i)]) % mod;
    
    rep(j, 0, 1 << m) dp[j] = binpow(2, dp[j]) - 1;

    rep(i, 0, m)
        rep(j, 0, 1 << m)
            if(j & (1 << i))
                dp[j] = ((dp[j] - dp[j ^ (1 << i)]) % mod + mod) % mod;
    
    printf("%d\n", dp[(1 << m) - 1]);

	return 0;
}

CF383E Vowels(子集dp)

首先枚举元音字母序列

对于当前的元音字母序列,是正确单词的就是与这个序列交集不为空的

发现这个交集不为空的不好处理,反过来想,计算交集为空的

交集为空的显然是元音序列的补集的子集

求子集的个数就是高维前缀和模板题了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int w = 24;
int dp[1 << w], n;

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
    {
        string s; cin >> s;
        int cur = 0;
        rep(i, 0, 3) cur |= 1 << (s[i] - 'a');
        dp[cur]++;
    }
    
    rep(i, 0, w)
        rep(j, 0, 1 << w)
            if(j & (1 << i))
                dp[j] += dp[j ^ (1 << i)];
    
    int ans = 0;
    rep(j, 0, 1 << 24)
    {
        int cur = ((1 << 24) - 1) ^ j;
        ans ^= (n - dp[cur]) * (n - dp[cur]);
    }
    printf("%d\n", ans);

	return 0;
}

E. Generate a String(线性dp + 分类讨论)

这道题的难点在于如何处理减去这个操作,我也是卡在了这里

突破口是发现减法的使用是由限制的

首先减法之前肯定是一次乘以2,不可能加1再减1,浪费

所以如果要用减法一定是乘以2再减1

那么这还不够

可以发现乘以2之后减法最多用1次

如果用两次的话,乘之前先减,再乘,是更优的

所以减法如果要使用只能乘以2再减1

那么这样的话就是i转移到i * 2 - 1

反过来就是(i + 1) / 2 显然i必须为奇数

所以分类讨论

当i为偶数的时候,要不从i-1来,要从i / 2来

当i为奇数的时候,要不从i - 1来 要不从(i + 1)/2来
 

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e7 + 10;
typedef long long ll;
ll dp[N], x, y;
int n;

int main()
{
    cin >> n >> x >> y;
    _for(i, 1, n) dp[i] = 1e18;
    _for(i, 1, n)
    {
        if(i % 2 == 0) dp[i] = min(dp[i - 1] + x, dp[i / 2] + y);
        else dp[i] = min(dp[i - 1] + x, dp[(i + 1) / 2] + x + y);
    }
    printf("%lld\n", dp[n]);

	return 0;
}

CF1294F Three Paths on a Tree(树的直径)

显然,如果只有两个点,那答案就是树的直径,因为是最长路径

那么可以猜结论,如果是三个点,那么就是先找到树的直径,然后再找一个点,使多出来的路径尽可能长。

注意可能找不到,也就是是一条链的情况,这种情况要特判一下

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
vector<int> g[N];
int d[N], f[N], vis[N], p, n, a, b, c, ans;

void dfs(int u, int fa, int op)
{
    if(op) f[u] = fa;
    for(int v: g[u])
    {
        if(v == fa || vis[v]) continue;
        d[v] = d[u] + 1;
        if(d[v] > d[p]) p = v;
        dfs(v, u, op);
    }
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0, 0); a = p;
    d[p] = 0; dfs(p, 0, 1);
    ans += d[p];
    b = p;
    for(int u = p; u; u = f[u]) vis[u] = 1;

    int t = p; p = 0;
    for(int u = t; u; u = f[u]) 
    {
        d[u] = 0;
        dfs(u, 0, 0);
    }
    ans += d[p];
    c = p;

    if(!c)
    {
        _for(i, 1, n)
            if(i != a && i != b)
            {
                c = i;
                break;
            }
    }
    printf("%d\n%d %d %d\n", ans, a, b, c);

	return 0;
}

C. First Digit Law(数位dp + 概率dp)

这题还行,是两个知识点凑起来的一道题

首先数位dp预处理,很模板了

然后就是概率dp

有n件事情,每件事情有发生的概率,求至少发生t次的概率

dp[i][j]表示前i件事情发生了j次的概率

当前事情发生或者不发生可以写出转移方程

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e3 + 10;
ll dp[20][2];
int a[20], len, n;
double p[N], f[N][N];

ll dfs(int pos, int val, int lead, int limit)
{
    if(pos > len) return val;
    if(dp[pos][val] != -1 && !limit && !lead) return dp[pos][val];
    ll res = 0, mx = limit ? a[len - pos + 1] : 9;
    _for(i, 0, mx)
    {
        if(!i && lead) res += dfs(pos + 1, val, lead, i == mx && limit);
        else if(i && lead) res += dfs(pos + 1, val | (i == 1), 0, i == mx && limit);
        else res += dfs(pos + 1, val, 0, i == mx && limit);
    }
    if(!limit && !lead) dp[pos][val] = res;
    return res;
}

ll part(ll x)
{
    len = 0;
    while(x) a[++len] = x % 10, x /= 10;
    memset(dp, -1, sizeof dp);
    return dfs(1, 0, 1, 1);
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) 
    {
        ll l, r;
        scanf("%lld%lld", &l, &r);
        p[i] = (double)(part(r) - part(l - 1)) / (r - l + 1);
    }

    f[1][1] = p[1];
    f[1][0] = 1 - p[1];
    _for(i, 2, n) 
        _for(j, 0, i)
            f[i][j] = f[i - 1][j] * (1 - p[i]) + f[i - 1][j - 1] * p[i];

    int k; scanf("%d", &k);
    int mi = (n * k + 99) / 100;
    double ans = 0;
    _for(i, mi, n) ans += f[n][i];
    printf("%.15f\n", ans);

	return 0;
}

CF883I Photo Processing(二分答案+dp)

很快就想到了二分答案,但是不知道这么O(n)的判断

然后又抛弃了二分答案,想了一个O(n^2)的dp 但是想了很久都不知道怎么优化

事实证明优化不了,思路错了

正解就是二分答案加O(n)的判断,用dp

因为只用判断合不合法,所以用dp[i]表示以i为结尾合不合法

那么显然dp[i] |= dp[j - 1]  j ~ i分为一组,要满足两个条件,一个是最大值减最小值小于等于mid,一个是长度符合。这样可以求出可转移的j的一个范围[l, r]

那么怎么做到O(n)呢,每次都又一个区间可以转移,每次重新扫一遍肯定超时

可以发现,对于一个dp[j - 1] 如果它不能转移,那么之后也不能转移

所以可以设置一个左端点l,不能转移的时候就加1,写个while循环

这个while循环最多执行n次,所以是O(n)的  

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 3e5 + 10;
int a[N], dp[N], n, m;

bool check(int key)
{
    memset(dp, 0, sizeof dp);
    dp[0] = 1;

    int l = 1, r;
    _for(i, 1, n)
    {
        while(a[i] - a[l] > key) l++;
        r = i - m + 1;
        while(!dp[l - 1] && l <= r) l++;
        if(l <= r) dp[i] = 1;
    }
    return dp[n];
}

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, n) scanf("%d", &a[i]);
    sort(a + 1, a + n + 1);

    int l = -1, r = 1e9;
    while(l + 1 < r)
    {
        int m = l + r >> 1;
        if(check(m)) r = m;
        else l = m;
    }
    printf("%d\n", r);

	return 0;
}

CF936B Sleepy Game(有向图的环)

这题充分考到了有向图的环

首先判断有没有到一个出度为0的点的奇数长度的路径

这里怎么处理访问过的节点非常重要

注意因为有了奇偶的区别,所以用vis[][0/1]来表示到达一个点的奇偶

同时在dfs的过程中记录路径

然后是从判断从当前点出发能不能找到一个环

有向图判环和无向图判环是有区别的

无向图只要访问到访问过的点就是环

有向图不一定,这时已经访问过的节点分为两种,一种是祖先节点,也就是它前面的节点,一种是非祖先节点。这时若要有环一定要是祖先节点,这样能到祖先节点又到它,从而形成环。非祖先节点的话,一定是它的子树全部访问完的,如果没有访问完那么它这个点一定包含在当前从根到当前点的路径。

所以要区分一下,就是当前节点的子树有没有访问完。没访问完的话就一定还在当前路径中,也就是祖先节点。

换句话说,一定要是访问的点还在栈里面才可以。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
vector<int> g[N], ans;
int out[N], vis[N][2], vis2[N];
int n, m, s, flag, draw;   

void dfs(int u, int cur)
{
    ans.push_back(u);
    if(!out[u] && cur)
    {
        flag = 1;
        return;
    }

    for(int v: g[u])
    {
        if(vis[v][cur ^ 1]) continue;
        vis[v][cur ^ 1] = 1;
        dfs(v, cur ^ 1);
        if(flag) return;
    }

    ans.pop_back();
}

void dfs2(int u)
{
    vis2[u] = 1;   //入栈
    for(int v: g[u])
    {
        if(vis2[v] == 1)
        {
            draw = 1;
            return;
        }
        dfs2(v);
        if(draw) return;
    }
    vis2[u] = 2; //出栈
}

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, n)
    {
        int a; scanf("%d", &a);
        while(a--)
        {
            int x; scanf("%d", &x);
            g[i].push_back(x);
            out[i]++;
        }
    }

    scanf("%d", &s);
    vis[s][0] = 1;
    dfs(s, 0);

    if(flag)
    {
        puts("Win");
        for(int u: ans) printf("%d ", u);
    }
    else 
    {
        dfs2(s);
        puts(draw ? "Draw" : "Lose");
    }   

	return 0;
}

CF1092F Tree with Maximum Cost(换根dp)

换根dp模板题

其实我之前有自己独立类似的思路,但是不知道这个叫做换根dp

两次dfs即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 2e5 + 10;
vector<int> g[N];
ll sum[N], d[N], a[N], ans, cur;
int n;

void dfs(int u, int fa)
{
    sum[u] = a[u];
    for(int v: g[u])
    {
        if(v == fa) continue;
        d[v] = d[u] + 1;
        dfs(v, u);
        sum[u] += sum[v];
    }
}

void dfs2(int u, int fa, ll val)
{
    ans = max(ans, val);
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs2(v, u, val - sum[v] + sum[1] - sum[v]);
    }
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%lld", &a[i]);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0);
    _for(i, 1, n) cur += a[i] * d[i];
    dfs2(1, 0, cur);
    printf("%lld\n", ans);

	return 0;
}

P3478 [POI2008]STA-Station(换根dp)

非常裸的换根dp题

我开始想到了树的重心

但是树的重心的性质是所有顶点到树的重心的距离和最小

树的重心就是最大子树节点数最小的点,可以一次dfs求。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e6 + 10;
vector<int> g[N];
int siz[N], d[N], n, p = 1;
ll ans, cur;

void dfs(int u, int fa)
{
    siz[u] = 1;
    for(int v: g[u])
    {
        if(v == fa) continue;
        d[v] = d[u] + 1;
        dfs(v, u);
        siz[u] += siz[v];
    }
}

void dfs2(int u, int fa, ll val)
{
    if(val > ans)
    {
        ans = val;
        p = u;
    }
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs2(v, u, val - siz[v] + n - siz[v]);
    }
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0);
    _for(i, 1, n) cur += d[i];
    dfs2(1, 0, cur);
    printf("%d\n", p);

	return 0;
}

CF510D Fox And Jumping(裴蜀定理 + 隐式图最短路)

这道题妙啊

用到了一个裴蜀定理

即任意ax + by一定是gcd(a,b)的倍数

ax + by = gcd(a, b)一定有解

对于这题而言,要走到任何一个地方,显然要产生1才行

对于两个数,即ax + by = 1  即gcd(a, b) = 1 也就是互质

对于多个数,推广一下,就是gcd和为1

那么现在问题就是用最小的代价买数,使得gcd和为1

这个过程,这个看作一个隐式图找最短路

边的费用就是买这个数的代价,可以达到新的gcd和的那个点

那么我们可以从0开始,gcd(0, x) = x然后跑最短路,答案就是d[1]
 

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 300 + 10;
struct node
{
    int v, w;
    bool operator < (const node& rhs) const
    {
       return w > rhs.w;
    }
};
int l[N], c[N], n;
unordered_map<int, int> d;

int gcd(int a, int b) { return !b ? a : gcd(b, a % b); }

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &l[i]);
    _for(i, 1, n) scanf("%d", &c[i]);

    priority_queue<node> q;
    d[0] = 0;
    q.push(node{0, d[0]});
    while(!q.empty())
    {
        node x = q.top(); q.pop();
        int u = x.v;
        if(d[u] != x.w) continue;
        
        _for(i, 1, n)
        {
            int v = gcd(u, l[i]);
            if(!d[v] || d[v] > d[u] + c[i])
            {
                d[v] = d[u] + c[i];
                q.push(node{v, d[v]});
            }
        }
    }
    printf("%d\n", d[1] ? d[1] : -1);

	return 0;
}

CF213C Relay Race(dp状态简化)

我想到了dp[x1][y1][x2][y2]的做法,但是这样时间空间都会炸

优化方法是让它们一步一步同时走,设k = x + y

dp[k][x][y] 这样时间空间都少了一维

权值有负数,要初始化为负数

每次走一步都是两个点同时走一步

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 300 + 10;
int dp[N << 1][N][N], a[N][N], n; //第一维两倍

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
        _for(j, 1, n)  
            scanf("%d", &a[i][j]);
    
    memset(dp, -0x3f, sizeof dp);
    dp[2][1][1] = a[1][1];
    _for(k, 3, 2 * n)
        _for(x1, 1, n)
            _for(x2, 1, n)
            {
                int y1 = k - x1, y2 = k - x2;
                if(y1 < 1 || y1 > n || y2 < 1 || y2 > n) continue; //注意这里
                int t = (x1 == x2) ? a[x1][y1] : (a[x1][y1] + a[x2][y2]);
                dp[k][x1][x2] = max(dp[k][x1][x2], t + dp[k - 1][x1][x2]);
                dp[k][x1][x2] = max(dp[k][x1][x2], t + dp[k - 1][x1 - 1][x2]);
                dp[k][x1][x2] = max(dp[k][x1][x2], t + dp[k - 1][x1][x2 - 1]);
                dp[k][x1][x2] = max(dp[k][x1][x2], t + dp[k - 1][x1 - 1][x2 - 1]); 
            }
    printf("%d\n", dp[2 * n][n][n]);

	return 0;
}

周五

CF900D Unusual Sequences(记忆化搜索)

这道题我已经想了一半了,式子推完了,但最后这个递归的思路没想到

很显然先y / x  就是求和为n的序列且gcd和为1有多少个

首先不考虑gcd和为1这个条件 看看有多少种

这个可以用隔板法,x个数,中间有x-1个可以插入的地方,可以插0到n-1个

求和之后为2 ^ (x - 1)

那么再减去gcd和不为一的。若一个数列gcd和为d,发现每一个数同除以d之后又是一个gcd和为1的数列

设f(n)为和为n的gcd和为1的序列个数

则f(n) = 2 ^ (n - 1) - f(n / d)

其中d > 1

我推到这里,发现算这个式子是O(n^3/2)的,然后就不知道怎么做了

事实上,我们只需要求f(n)的值,并不需要把所有值都求出来

所以可以用递归,记忆化搜索的思路,可以发现,它的因子的因子还是它的因子

所以复杂度是d(n) * 根号n的 可以接受

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e9 + 7;
unordered_map<int, int> mp;

int add(int a, int b) { return (a + b) % mod; }
int mul(int a, int b) { return 1LL * a * b % mod; }
int sub(int a, int b) { return (a - b % mod + mod) % mod; }

int binpow(int a, int b)
{
    int res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = mul(res, a);
        a = mul(a, a);
    }
    return res;
}

int dfs(int x)
{
    if(mp[x]) return mp[x];
    if(x == 1) return 1;
    int res = binpow(2, x - 1);
    res = sub(res, dfs(1));
    for(int i = 2; i * i <= x; i++)
        if(x % i == 0)
        {
            res = sub(res, dfs(i));
            if(i * i != x) res = sub(res, dfs(x / i));
        }
    return mp[x] = res;
}

int main()
{
    int x, y;
    scanf("%d%d", &x, &y);
    if(y % x != 0) puts("0");
    else printf("%d\n", dfs(y / x));
	return 0;
}

CF372B Counting Rectangles is Fun(dp打表)

这题妙啊

首先矩阵范围很小,询问范围很大

可以考虑直接dp打表,把答案打出来

dp[x1][y1][x2][y2]表示左上角(x1, y1) 右下角(x2, y2)的方案数

那么转移方程就类似二维前缀和,关键是怎么处理因为多了(x2, y2)这个方格而多出的矩形

多出的矩形的右下角一定是这个方格

我们可以枚举这些新矩阵的宽,从1一直增大,可以发现,当宽固定的时候,高一定是能取到的高度的前缀最小值(画个图就知道了) 所以预处理一下就可以了

注意初始化的时候要初始化成目前要求的数组的高,而不是预处理的高

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 50;
int dp[N][N][N][N], a[N][N], n, m, q;

int main()
{
    scanf("%d%d%d", &n, &m, &q);
    _for(i, 1, n)
        _for(j, 1, m)
        {
            int x; scanf("%1d", &x);
            if(x) a[i][j] = 0;
            else a[i][j] = a[i - 1][j] + 1;
        }
    
    _for(x1, 1, n)
        _for(x2, x1, n)
            _for(y1, 1, m)
                _for(y2, y1, m)
                {
                    int cur = x2 - x1 + 1; //初始化注意
                    for(int y = y2; y >= y1; y--)
                    {   
                        cur = min(cur, a[x2][y]);
                        dp[x1][y1][x2][y2] += cur;
                    }   
                    dp[x1][y1][x2][y2] += dp[x1][y1][x2 - 1][y2] + dp[x1][y1][x2][y2 - 1]
                                         - dp[x1][y1][x2 - 1][y2 - 1];
                }
    
    while(q--)
    {
        int a, b, c, d;
        scanf("%d%d%d%d", &a, &b, &c, &d);
        printf("%d\n", dp[a][b][c][d]);
    }

	return 0;
}

E. Pashmak and Graph(dp转移顺序与无后效性)

如果用dp[i]表示以i为结尾的最长路径

发现有后效性,选了什么边会影响后面

怎么避免呢

把边排序,从小到大,这样就不会有后效性了,按照边的顺序来转移

注意是严格递增的,所以边权相等的边转移的时候可能互相影响

这个时候用一个临时数组存一存,然后再把结果合并回去

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 3e5 + 10;
int dp[N], t[N], n, m;
struct node
{
    int u, v, w;
    bool operator < (const node& rhs) const
    {
        return w < rhs.w;
    }
};
vector<node> ve;

int main()
{
    scanf("%d%d", &n, &m);
    while(m--)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        ve.push_back(node{u, v, w});
    }

    sort(ve.begin(), ve.end());
    rep(i, 0, ve.size())
    {
        int j = i;
        while(j + 1 < ve.size() && ve[j + 1].w == ve[i].w) j++;
        _for(k, i, j) t[ve[k].v] = 0;
        _for(k, i, j) t[ve[k].v] = max(t[ve[k].v], dp[ve[k].u] + 1);
        _for(k, i, j) dp[ve[k].v] = max(dp[ve[k].v], t[ve[k].v]);
        i = j;
    }

    int ans = 0;
    _for(i, 1, n) ans = max(ans, dp[i]);
    printf("%d\n", ans);

	return 0;
}

E. Vasya and Good Sequences(观察限制)

先全部转化为二进制下1的个数

首先发现要满足异或和要两个条件,一个是和为偶数,一个是最大值要小于等于剩余值的和

可以发现,只要满足这两个条件,一定可以异或和为0(反证法)

那么怎么找序列长度呢

第一个条件好满足,用前缀和即可,但是我卡在第二个条件上,不知道怎么利用

实际上突破口在于发现最大值也就是64,这意味着如果其他数超过了64个,那么肯定满足最大值小于等于剩余值的和

所以我们可以枚举右端点,先算第一个条件下的序列,然后暴力从右端点向左查找64个端点看是不是符合第二个条件,不符合则ans--

以前也做过类似的题,也就是序列长度大于某一个阈值的时候一定是满足条件的,所以可以小范围暴力

再算第一个条件的时候,要满足区间和为偶数,可以将前缀和模2,然后看之前右多少个和当前前缀和模2的值相同,用cnt[0/1]记录一下即可

注意sum[0] = 0 所以一开始初始化的时候cnt[0] = 1

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 3e5 + 10;
int n, a[N], sum[N], cnt[2];

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
    {
        ll x; scanf("%lld", &x);
        while(x)
        {
            a[i] += x % 2;
            x /= 2;
        }
    }

    ll ans = 0;
    cnt[0] = 1;                                  //注意这个 因为计算的时候要用到sum[0] 
    _for(i, 1, n)
    {
        sum[i] = sum[i - 1] + a[i];
        ans += cnt[sum[i] % 2];

        int mx = 0;
        for(int k = i; k >= max(i - 64, 1); k--) //k为左端点
        {
            mx = max(mx, a[k]);
            if(sum[k - 1] % 2 == sum[i] % 2 && mx > (sum[i] - sum[k - 1]) / 2) ans--;
        }
        cnt[sum[i] % 2]++;
    }
    printf("%lld\n", ans);

	return 0;
}

C. Bear and Tree Jumps(换根dp)

这题要考虑的细节比较多

首先若k = 1 就是很裸的换根dp题了 有提示

如果我们先算出以1为根的答案,我们考虑换根时答案会怎么变换

可以发现,从u换到v,v的子树里面距离u为nk + 1的点会次数减1,其补树里面距离u为nk的点的次数加1

所以为了得出值的变化,我们需要计算出u有多个距离其为nk和nk+1的点

这显然和距离模k的余数有关,我们需要在换根时维护这个值

首先第一次dfs,求出每个点的深度,可以求出根为1时的值。同时需要求出siz[u][j] 表示u的子树中离u的距离模k为j的节点数。

第二次dfs,我们需要维护f[u][j] 表示所有点离u的距离模k为j的节点数

总的来说,维护siz数组 f数组 

siz数组的话比较好维护,第一次dfs的时候siz[u][j] = siz[v][j - 1]

f数组需要画一画图

u换根到v时由u的f数组计算v的f数组  现在要计算f[v][j]

v的f数组分为两个部分,一个部分是v的子树,一个部分是剩下的补树

对于v的子树,直接就是siz[v][j]

对于剩下的补树,要利用u。距离v为j,那么距离u就为j-1 也就是f[u][j - 1]
发现f[u][j - 1]多了一部分,即v的子树的这一部分,那么我们要减去这部分

距离u为j - 1 那么距离v就是j - 2 也就是siz[v][j - 2]

因此f[v][j] = siz[v][j] + f[u][j - 1] - siz[v][j - 2]

两个数组维护好了,接下来看怎么更新值

u到v的时候,v的子树里面距离u为nk+1的会减一

也就是距离v为nk

因此这一部分的节点数为siz[v][0]

另一部分,补树的部分,也就是距离u为nk的会加一

这部分节点数就是f[u][0] - siz[v][k - 1](减去多余的部分)

因此改变的量就是 siz[v][0] + f[u][0] - siz[v][k - 1]

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 2e5 + 10;
vector<int> g[N];
int d[N], siz[N][5], f[N][5], n, k;
ll ans;

void dfs(int u, int fa)
{
    siz[u][0] = 1;
    for(int v: g[u])
    {
        if(v == fa) continue;
        d[v] = d[u] + 1;
        dfs(v, u);
        rep(i, 0, k) siz[u][i] += siz[v][(i - 1 + k) % k];
    }
}

void dfs2(int u, int fa, ll cur)
{
    ans += cur;
    for(int v: g[u])
    {
        if(v == fa) continue;
        rep(i, 0, k)
            f[v][i] = siz[v][i] + f[u][(i - 1 + k) % k] - siz[v][(i - 2 + k) % k];
        dfs2(v, u, cur - siz[v][0] + f[u][0] - siz[v][k - 1]);
    }
}

int main()
{
    scanf("%d%d", &n, &k);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0);
    ll cur = 0;
    _for(i, 1, n) cur += (d[i] + k - 1) / k;

    rep(i, 0, k) f[1][i] = siz[1][i];
    dfs2(1, 0, cur);
    printf("%lld\n", ans / 2);

	return 0;
}

B. Jeff and Furik(逆序对)

这道题的关键在于用逆序对来思考问题

有序就是逆序对为0

第一个操作最优肯定减少一个逆序对

第二个操作 1/2减少一个,1/2增加一个

所以两个操作下来期望减少一个逆序对 -1 - 1 / 2 + 1 / 2

那么答案就是逆序对个数乘以2

但是注意,如果是奇数,最后一个逆序对执行第一个操作就结束了,还要减一

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 3e3 + 10;
int a[N], n, cnt;

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) 
    {
        scanf("%d", &a[i]);
        _for(j, 1, n - 1)
            if(a[j] > a[i])
                cnt++;
    }
    if(cnt % 2 == 1) printf("%d\n", 2 * cnt - 1);
    else printf("%d\n", 2 * cnt);
    
	return 0;
}

周六

B. Destruction of a Tree(思维)

非常思维的一道题

首先一次删点只能去掉偶数边,所以一共由奇数条边是肯定不行的,可以先判断一下

然后从简单的入手,叶子度数为1,先要删它必须删它父亲

父亲不一定能删,不能删的话就要删它的父亲

所以我们从下往上先把能删除的点删掉

然后再从上往下把剩下的点删掉 如果这时还由奇数度数的点,一定不行。因为其父亲已经遍历过的,其孩子在第一轮没被删掉说明是奇数的,这时这个点就删不掉。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
vector<int> g[N], ans;
int deg[N], vis[N], n, flag;

void del(int u)
{
    ans.push_back(u);
    deg[u] = 0; vis[u] = 1;
    for(int v: g[u]) deg[v]--;
}

void dfs(int u, int fa)
{
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs(v, u);
    }
    if(deg[u] % 2== 0) del(u);
}

void dfs2(int u, int fa)
{
    if(!vis[u])
    {
        if(deg[u] % 2 == 0) del(u);
        else flag = 1;
    } 

    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs2(v, u);
    }
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        if(!x) continue;
        g[x].push_back(i);
        g[i].push_back(x);
        deg[i]++; deg[x]++;
    }

    if(n % 2 == 0)
    {
        puts("NO");
        return 0;
    }

    dfs(1, 0);
    dfs2(1, 0);

    if(flag) puts("NO");
    else
    {
        puts("YES");
        for(int x: ans) printf("%d\n", x);
    }
    
	return 0;
}

D. Binary Spiders(异或结论+字典树优化dp)

这题搞了挺久,但挺有收获的

首先有一个结论,一个序列,两两异或的最小值,一定是排序之后相邻异或的最小值

也就是这个最小值一定出现在大小相邻的数异或,这是一个结论

根据这个可以先排序,然后用类似最长上升子序列的思想,dp[i]表示以i为结尾的集合的最大数目

dpi = max(dpj + 1)  a[i] ^ a[j] >= k

这样做是O(n^2)的

考虑怎么优化,关键是a[i] ^ a[j] >= k

涉及到异或,可以用字典树来优化

对于当前的a[i],可以在字典树上跳,如果k的当前位是1,那就跳a[i]当前位的相反位,如果k的当前位是0,那么就跳a[i]当前位的相同位,同时a[i]的相反位之后所有的数都符合条件

所以我们可以把输入的数建立字典树,多存一个信息,字典树上节点p的子树中,dp值是多少。因为要输出路径,还要存是哪一个点,这里用pair实现会比较方便

具体实现的时候,也就加入树的时候多了一行代码,也就是将经过的点的值都取最大值即可

最后输出路径的时候,注意要区分原来的下标和现在的下标,建立一个映射即可,知道现在的下标对应原来的哪个下标,输出的是原来的下标

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 3e5 + 10;
pair<int, int> dp[N], a[N], v[N * 30];             //用pair处理很方便
int t[N * 30][2], n, k, cnt;

void update(int i)
{
    int p = 0;
    pair<int, int> mx;                             //找到最大的dpj
    for(int j = 29; j >= 0; j--)
    {
        int idx = a[i].first >> j & 1, idk = k >> j & 1;
        if(!idk) 
        {
            if(t[p][idx ^ 1]) 
                mx = max(mx, v[t[p][idx ^ 1]]);
            p = t[p][idx];
        }
        else p = t[p][idx ^ 1];
        if(!p) break;
    }
    if(p) mx = max(mx, v[p]);
    dp[i] = {mx.first + 1, mx.second};     
}

void add(int i)
{
    int p = 0;
    pair<int, int> cur = {dp[i].first, i};  //dp的second存的是前一个转移的值
    for(int j = 29; j >= 0; j--)            //加入的时候second是i
    {
        int idx = a[i].first >> j & 1;
        if(!t[p][idx]) t[p][idx] = ++cnt;
        p = t[p][idx];
        v[p] = max(v[p], cur);              //最大值
    }
}

int main()
{
    scanf("%d%d", &n, &k);
    _for(i, 1, n)
    {
        scanf("%d", &a[i].first);
        a[i].second = i;
    }
    sort(a + 1, a + n + 1);
    _for(i, 1, n) update(i), add(i);
    
    int mx = 0, pos;
    _for(i, 1, n) 
        if(mx < dp[i].first)
        {
            mx = dp[i].first;
            pos = i;
        }
    if(mx == 1) puts("-1");
    else
    {
        printf("%d\n", mx);
        while(pos)
        {
            printf("%d ", a[pos].second);       //输出原来的下标
            pos = dp[pos].second;
        }
    }
    
	return 0;
}

E1. Cats on the Upgrade (easy version)(树)

这题搞了挺久终于弄懂了

这个括号一层一层嵌套,可以联想到树的结构,我们现在把它转化成一颗树

首先有一个初始节点,遇到一个左括号就向下一层建立一个节点

这个节点代表的是这个左括号和匹配它的右括号。

遇到右括号就向上一层,就这样建树。

实际操作的时候可以简化,用左括号的位置代表这个节点,一个节点的父亲一定是之前在栈顶的左括号。因此遇到左括号就栈顶向当前节点连边即可。

然后考虑怎么求值,一个节点的值表示的是扩号内部的匹配数量,不包括括号本身。

那么可以发现叶子节点的值为0。对于一个节点,它的子节点就是(RBS)(RBS)(RBS)……

那么它的值就是子节点的值之和,也就是各个的RBS之和,加上k * (k + 1) / 2

k为孩子数量。可以看成n个括号,取一段的话就是插两个版,那么有n + 1的位置可以插,就是n+1个位置取两个

那么这样子树形dp就可以求出每个节点的值

然后现在问题是怎么处理询问

给的询问一定是一个匹配的,那么可以发现这一段匹配的最浅的一层一定是同一层从左到右连续的孩子。

于是预处理出前缀和以及每个节点的父亲,对于输入l, r

首先找到父亲,也就是fa[l] 然后左端点就是id[l]  右端点是id[match[r]]

注意是按照左端点来代表点,所以要是相应匹配的左端点

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 3e5 + 10;
vector<int> g[N];
vector<ll> sum[N];
char s[N];
int st[N], match[N], vis[N], fa[N], id[N];
int top, n, q;
ll dp[N];

ll f(int x)
{
    return 1LL * x * (x + 1) / 2;
}

void dfs(int u)
{
    for(int v: g[u])
    {
        dfs(v);
        dp[u] += dp[v];
        id[v] = sum[u].size();
        sum[u].push_back(dp[v]);
    }
    dp[u] += f(g[u].size());
    rep(i, 1, sum[u].size()) sum[u][i] += sum[u][i - 1];
}
 
int main()
{
    scanf("%d%d", &n, &q);
    scanf("%s", s + 1);

    _for(i, 1, n)
    {
        if(s[i] == '(') st[++top] = i;
        else if(top)
        {
            vis[i] = vis[st[top]] = 1;
            match[i] = st[top];
            match[st[top]] = i;
            top--;
        }
    }

    top = 0;
    _for(i, 1, n)
    {
        if(!vis[i]) continue;
        if(s[i] == '(')
        {   
            fa[i] = st[top];
            g[fa[i]].push_back(i);
            st[++top] = i;
        }
        else top--;
    }
    dfs(0);

    while(q--)
    {
        int op, l, r;
        scanf("%d%d%d", &op, &l, &r);
        int u = fa[l], ql = id[l], qr = id[match[r]];
        ll ans = sum[u][qr] + f(qr - ql + 1);
        if(ql) ans -= sum[u][ql - 1];
        printf("%lld\n", ans);
    }
    
	return 0;
}

E. Bertown roads(割边)

画了一下图,发现有割边是一定不可以的

然后发现如果没有割边就是一定可以的,因为任意两个点都可以通过一条路径过去,一条路径回来

所以我先用tarjan判断有无割边

关于如何确定方向,就随便找一个点dfs,按照dfs的顺序来确定方向

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
vector<int> g[N];
int dfn[N], low[N], vis[N], cnt, n, m, flag;
map<pair<int, int>, bool> mp;

void dfs(int u, int fa)
{
    dfn[u] = low[u] = ++cnt;
    for(int v: g[u])
    {
        if(v == fa) continue;
        if(!dfn[v])
        {
            dfs(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v] > dfn[u]) flag = 1;
        }
        else low[u] = min(low[u], dfn[v]);
    }
}

void dfs2(int u, int fa)
{
    vis[u] = 1;
    for(int v: g[u])
    {
        if(v == fa) continue;
        if(!mp[{min(u, v), max(u, v)}])
        {
            printf("%d %d\n", u, v);
            mp[{min(u, v), max(u, v)}] = 1;
        }
        if(!vis[v]) dfs2(v, u);
    }
}
 
int main()
{
    scanf("%d%d", &n, &m);
    while(m--)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);

    if(flag) 
    {
        puts("0");
        return 0;
    }

    dfs2(1, 0);
    
	return 0;
}

周日

B. Appleman and Tree(树形dp)

树形dp好题

dp[u][0 / 1]表示u的子树中,u在的联通块有0/1个黑色节点的方案数

分类当前点为黑白讨论即可

处理的过程中要先搜一遍得出一些信息,然后再转移

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e9 + 7;
const int N = 1e5 + 10;
vector<int> g[N];
int dp[N][2], col[N], n;

int mul(int a, int b) { return 1LL * a * b % mod; }
int add(int a, int b) { return (a + b) % mod; }

int binpow(int a, int b)
{
    int res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = mul(res, a);
        a = mul(a, a);
    }
    return res;
}

int inv(int x) { return binpow(x, mod - 2); }

void dfs(int u, int fa)
{
    int cur = 1;
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs(v, u);
        cur = mul(cur, dp[v][0] + dp[v][1]);
    }

    if(col[u] == 1)
    {
        dp[u][0] = 0;
        dp[u][1] = cur;
    }
    else
    {
        dp[u][0] = cur;
        for(int v: g[u])
        {
            if(v == fa) continue;
            dp[u][1] = add(dp[u][1], mul(dp[v][1], mul(cur, inv(dp[v][1] + dp[v][0]))));
        }
    }
}
 
int main()
{
    scanf("%d", &n);
    _for(i, 2, n)
    {
        int x; scanf("%d", &x);
        x++; 
        g[x].push_back(i);
        g[i].push_back(x);
    }
    _for(i, 1, n) scanf("%d", &col[i]);

    dfs(1, 0);
    printf("%d\n", dp[1][1]);

	return 0;
}

D. Count Good Substrings(观察)

妙啊 这题主要是观察出一个性质

我一开始用dp的思路,后来用组合数学的思路,都没有结果……

合并之后为abababa

任何一个字符串合并之后一定为a 或 aba或ababa……

可以发现,第一个字母和最后一个字母一定是相同的

也就是说,任意一个子串,只要首末字母相同,就一定是回文串

根据这个性质,求一个分奇数位置偶数位置的前缀和就可以了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e5 + 10;
int cnt[2][2], n;
char s[N];
 
int main()
{
    scanf("%s", s + 1);
    n = strlen(s + 1);

    ll odd = 0, even = 0;
    _for(i, 1, n)
    {
        int cur = (s[i] == 'a') ? 0 : 1;
        if(i % 2 == 0)            //当前在偶数位置
        {
            cnt[0][cur]++;
            odd += cnt[0][cur];
            even += cnt[1][cur];
        }
        else
        {
            cnt[1][cur]++;
            odd += cnt[1][cur];
            even += cnt[0][cur];
        }
    }
    printf("%lld %lld\n", even, odd);

	return 0;
}

B. Wonder Room(数学+暴力)

妙啊

这题的关键是在于只需要暴力枚举根号6n

首先假设a < b 这很重要

设新的长为a1 宽为b1

a1的限制比b1更小

然后就暴力枚举a1,然后算出b1

可以发现a1和b1是一一对应的

怎么优化这个暴力呢,可以发现有两个限制

一.只要b1小于b就可以break了

二.因为a1 和 b1是一一配对的,且a1的限制比b1更小,于是当a1 > b1的时候,这种情况已经在之前枚举过了,是对称的。所以一直保证a1 >= b1 这样时间复杂度就是根号6n的

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
ll n, a, b, ans = 1e18, ansa, ansb;
int flag;
 
int main()
{
    scanf("%lld%lld%lld", &n, &a, &b);
    if(a > b) swap(a, b), flag = 1;
    if(a * b >= 6 * n)
    {
        printf("%lld\n%lld %lld\n", a * b, a, b);
        return 0;
    }

    for(ll a1 = a; ; a1++)
    {
        ll b1 = (6 * n + a1 - 1) / a1;
        if(a1 > b1 || b1 < b) break;
        if(a1 * b1 < ans)
        {
            ans = a1 * b1;
            ansa = a1;
            ansb = b1;
        }
    }
    if(flag) swap(ansa, ansb);
    printf("%lld\n%lld %lld\n", ans, ansa, ansb);

	return 0;
}

F. SUM and REPLACE(欧拉筛+线段树)

秒切 因为这道题涉及到的两个知识点我都很熟悉

一个是log时间内质因数分解,这个要求输入的数的最大值在1e7以下,用欧拉筛存每个数的最小质因子

一个是线段树,我做过一道区间每个数求根号的,和这道题非常像,因为数下降的非常快,几次就处于不变的值,所以可以额外存一个量表示它会不会再变化

#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 3e5 + 10;
const int M = 1e6 + 10;

int vis[M], k[M], f[N << 4], n, m;
ll t[N << 4];
vector<int> p;

int get(int x)
{
    int res = 1;
    while(x > 1)
    {
        int t = k[x], cnt = 0;
        while(x % t == 0)
        {
            x /= t;
            cnt++;
        }
        res *= (cnt + 1);
    }
    return res;
}

void init()
{
    vis[0] = vis[1] = true;
    rep(i, 2, M)
    {
        if(!vis[i])
        {
            p.push_back(i);
            k[i] = i;
        }
        for(int x: p)
        {
            if(i * x >= M) break;
            vis[i * x] = true;
            k[i * x] = x;
            if(i % x == 0) break;
        }
    }
}

void up(int k)
{
    t[k] = t[l(k)] + t[r(k)];
    f[k] = f[l(k)] && f[r(k)];
}

void build(int k, int l, int r)
{
    if(l == r)
    {
        scanf("%lld", &t[k]);
        f[k] = (t[k] <= 2);
        return;
    }
    int m = l + r >> 1;
    build(l(k), l, m);
    build(r(k), m + 1, r);
    up(k);
}

void change(int k, int l, int r, int L, int R)
{
    if(f[k]) return;
    if(l == r)
    {
        t[k] = get(t[k]);
        f[k] = (t[k] <= 2);
        return;
    }

    int m = l + r >> 1;
    if(L <= m) change(l(k), l, m, L, R);
    if(R > m) change(r(k), m + 1, r, L, R);
    up(k);
}

ll query(int k, int l, int r, int L, int R)
{
    if(L <= l && r <= R) return t[k];
    ll res = 0;
    int m = l + r >> 1;
    if(L <= m) res += query(l(k), l, m, L, R);
    if(R > m) res += query(r(k), m + 1, r, L, R);
    return res;
}
 
int main()
{
    init();
    scanf("%d%d", &n, &m);
    build(1, 1, n);
    while(m--)
    {
        int op, l, r;
        scanf("%d%d%d", &op, &l, &r);
        if(op == 1) change(1, 1, n, l, r);
        else printf("%lld\n", query(1, 1, n, l, r));
    }

	return 0;
}

D. Berserk And Fireball(贪心)

这道题的核心就是贪心,可以先算出两种操作的性价比,每次优先选泽性价比高的操作,然后分类讨论即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 2e5 + 10;
int a[N], b[N], n, m, flag, op;
ll ans, k, x, y;

ll cal(int l, int r)
{
    if(l + 1 == r) return 0;
    int mx = 0, len = (r - 1) - (l + 1) + 1;
    _for(i, l + 1, r - 1) mx = max(mx, a[i]);

    if(mx < max(a[l], a[r]))
    {
        if(op == 1) return y * len;
        else return (len / k) * x + (len % k) * y;
    }
    else
    {
        if(len < k) flag = 1;
        else
        {
            if(op == 1) return x + (len - k) * y;
            else return (len / k) * x + (len % k) * y;
        }
    }
}

int main()
{
    scanf("%d%d%lld%lld%lld", &n, &m, &x, &k, &y);
    _for(i, 1, n) scanf("%d", &a[i]);
    _for(i, 1, m) scanf("%d", &b[i]);

    int p = 1;
    vector<int> pos;
    pos.push_back(0);
    _for(i, 1, n)
        if(a[i] == b[p])
        {
            pos.push_back(i);
            p++;
        }
    pos.push_back(n + 1);
    
    if(p != m + 1)
    {
        puts("-1");
        return 0;
    }

    if(y * k < x) op = 1;
    else op = 2;
    rep(i, 1, pos.size()) ans += cal(pos[i - 1], pos[i]);
    if(flag) puts("-1");
    else printf("%lld\n", ans);

	return 0;
}

E2. String Coloring (hard version)(逆序对)

这道题很顺利切掉了,受前几天做的一道逆序对题的启发

排序,交换,一次交换可以减少一个逆序对,这就是核心

这道题说颜色不同的才可以交换。可以发现,在排序的过程中,逆序对是一定要交换的,所以逆序对的颜色一定不一样。

因此对于当前的字母填什么颜色,可以看前面比它大的字母,前面比它大的字母构成的区间一定是[1, m] 也就是1到m的一段连续的,因为它们之间的限制是它们自己。

所以这个字母就可以填m + 1

因为只有26个字母,所以可以存一下每一个字母颜色最大值来求

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int ans[N], len, b[30], n;
char s[N];

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

    int res = 0;
    _for(i, 1, n)
    {
        int mx = 0;
        for(int j = s[i] + 1; j <= 'z'; j++)
            mx = max(mx, b[j - 'a']);
        ans[i] = mx + 1;
        b[s[i] - 'a'] = max(b[s[i] - 'a'], ans[i]);
        res = max(res, ans[i]);
    }

    printf("%d\n", res);
    _for(i, 1, n) printf("%d ", ans[i]);

	return 0;
}

F. Number of Subsequences(递推)

这题妙啊

一开始的思路就是枚举中间的b,然后每次迅速算出前面取一个a有多少方案,后面取一个c有多少方案

我一开始是手推公式的,发现挺复杂的,因为有问号的存在不好处理

后来发现直接用递推的思路来算,会好很多,而不是从一个整体来推公式

a   1

aa 2

aa? 7

先写出了这几个式子,关键是这个问好

aa?即aac aab aaa

这个问好的作用就是把前面的a的个数乘以3,然后加上当前问号取a多出来的1

再继续看 aa?a

这时答案是10,因为之前的一个问好使得变成了三个序列,加上一个a相当于加上了三个a

那么递推公式就很明显了,用cur表示当前有多少个序列 a[i]表示1到i有多少个取a的方案

s[i] = a   a[i] = a[i - 1] + cur   也就是加上序列个数

s[i] = ?    a[i] = a[i - 1] * 3 + cur; cur *= 3     加上一个问号就是把之前的乘上3,加上当前这个问号导致的a。注意,当前问号导致的a是  cur * 3 / 3   cur * 3是序列个数,然后只有三分之一是a,在除以3

c的后缀类似

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
const int mod = 1e9 + 7;
int a[N], c[N], n;
char s[N];

int add(int a, int b) { return (a + b) % mod; }
int mul(int a, int b) { return 1LL * a * b % mod; }

int main()
{
    scanf("%d%s", &n, s + 1);
    
    int len = 1;
    _for(i, 1, n) 
    {
        if(s[i] == 'a') a[i] = add(a[i - 1], len);
        else if(s[i] == '?')
        {
            a[i] = mul(a[i - 1], 3);
            a[i] = add(a[i], len);
            len = mul(len, 3);
        }
        else a[i] = a[i - 1];
    }
    
    len = 1;
    for(int i = n; i >= 1; i--) 
    {
        if(s[i] == 'c') c[i] = add(c[i + 1], len);
        else if(s[i] == '?')
        {
            c[i] = mul(c[i + 1], 3);
            c[i] = add(c[i], len);
            len = mul(len, 3);
        }
        else c[i] = c[i + 1];
    }

    int ans = 0;
    _for(i, 2, n - 1)
        if(s[i] == 'b' || s[i] == '?')
            ans = add(ans, mul(a[i - 1], c[i + 1]));
    printf("%d\n", ans);

	return 0;
}

D. Omkar and Medians(找规律)

这题一开始啥思路都没有

然后就对着样例找规律,拿样例试试

发现中位数都是连在一起的

第一组样例6 2 1 3为什么不行

因为前三个形成连续的1 2 6 而3插在了上一个中位数之外的位置

可以发现,每次插入一定要插在上一个中位数的两侧,这样才可以

然后是怎么处理重复的中位数的问题,我在这里WA了两发,没仔细想清楚

插入重复的数字相当于原来连续的中位数序列是没有变的

所以只要找到位置,它在上一个中位数的两侧,或者是中位数本身就可以了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int b[N], n;

int main()
{   
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) scanf("%d", &b[i]);

        set<int> s;
        s.insert(b[1]);
        int flag = 1;
        _for(i, 2, n)
        {
            if(b[i] == b[i - 1]) continue;
            s.insert(b[i]);

            auto it = s.upper_bound(b[i - 1]);
            if(it != s.end() && *it == b[i]) continue;
            it = s.lower_bound(b[i - 1]);
            if(it != s.begin() && *(--it) == b[i]) continue;
            flag = 0; break;
        }
        puts(flag ? "YES" : "NO");
    }
   
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值