2023牛客寒假算法基础集训营2

《重点考察容斥原理的题目》

  

 

 《L. Tokitsukaze and Three Integers》

 

 可以看的出:

  n很小,首先考虑暴力的方法:

    我们可以用两层for循环,将(ai*aj)%p 会等于什么求出来

    然后再用两层for循环枚举  x 和 ak

    看一下有多少个(ai*aj)%p 会对应上 (x-ak+p)%p

    (这里x-ak+p,写成这样是防止负数的产生)

    

    上面是完全不考虑    这个条件的情况

 

 

    上面的暴力,会导致在枚举ak的时候,可能ai*aj中:ai==ak 或者 aj==ak

    要解决这个情况也很简单:

      只要ans-= (ai*aj中ai==ak这种情况的个数+ai*aj中aj==ak这种情况的个数)

      我们可以用一个数组d[i][x]表示在有ai参加运算的情况下,ai*另一个数=x的个数

      那么ans-=d[k][(ai*aj)%p]即可

      

      d[i][x]在两层for循环枚举ai和aj的时候可以处理:

      

 

 

 

 

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 5001;
int n, p, a[N];
// cnt[x]:表示在a[]中有多少个(ai*aj)%p(i!=j)==x
// d[i][x]:表示有多少个是ai参与了组成了x
int cnt[N], d[N][5000];
int main()
{
    cin >> n >> p;
    for (int i = 1; i <= n; i++)
    {
        int num;
        scanf("%d", &num);
        a[i] = num % p;
    }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
        {
            if (i == j)
                continue;
            int res = ((ll)a[i] * a[j]) % p;
            cnt[res]++;
            // 为何这里是+=2,因为可能res=a[i]*a[j],也可能a[j]*a[i];
            d[i][res] += 2;
        }

    for (int x = 0; x <= p - 1; x++)
    {
        ll ans = 0;
        for (int k = 1; k <= n; k++)
        {
            int res = (x - a[k] + p) % p;
            ans += cnt[res] - d[k][res];
        }
        cout << ans << " ";
    }
    cout << endl;
    return 0;
}

 

 

《Tokitsukaze and a+b=n (hard)》

 

 

 

 

 

 

 

 

 

 

 

 

 像这样题目一开始给出很多个区间,十分难想:

  我们可以将 区间全部下压到一条x轴上

  只要x轴上的数(num)在某个区间上,那么cnt[num]++

  最终这个cnt[num]表示:

    num在多少个区间出现过

 

  这里要求出cnt[]数组来显然不能用暴力,而是要点技巧:

    对于给一段连续区间上的全部数进行+操作的快捷技巧:

      差分

      我们设置一个差分数组ca[],然后对于区间[l,r]

      ca[l]++,ca[r+1]--

      再对差分数组求一边前缀和,即可得cnt[]数组

  

  这个时候如果不考虑这个条件

 

 

    那么答案就是:c[a]*c[n-a]

    a用for循环枚举

    这个含义就是从c[a]个区间选出数a来,再从c[n-a]区间选出数b来,a+b=n

    

  但是我们要考虑

 

 

    按照容斥原理:我们只要用ans-=(i==j,a+b==n的个数来即可)

 

    

 

    即对于每一个区间,我们都要找到有多少个a+b==n的对数

 

    这个其实就是Tokitsukaze and a+b=n (medium)的做法

 

 

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 4 * 1e5 + 2, p = 998244353;
int n, m;
ll ca[N];
ll solve(int l1, int r1, int l2, int r2)
{
    ll l = l1, r = r1, nb = -1;
    while (l <= r)
    {
        ll mid = (l + r) >> 1;

        ll b = n - mid;
        // 说明mid太大了
        if (b < l2)
            r = mid - 1;
        else if (b > r2)
            l = mid + 1;
        else
        {
            nb = b;
            break;
        }
    }
    if (nb == -1)
        return 0;
    ll na = n - nb, ans = 0;
    ans++;
    ans += min(r1 - na, nb - l2);
    ans += min(na - l1, r2 - nb);
    return ans;
}
int main()
{
    cin >> n >> m;
    ll same = 0;
    for (int i = 1; i <= m; i++)
    {
        int l, r;
        cin >> l >> r;
        // 处理相同区间中元素相加使得为n的方案对数
        same = (same + solve(l, r, l, r)) % p;
        ca[l]++, ca[r + 1]--;
    }
    // 来一遍前缀和,看一下每个数在多少个不一样的区间出现过
    // 这里直接出鬼了,这里一定要写i<=N
    // 而不能写i<=200000,但是题目数据写的明明是1<=l,r<=200000
    for (int i = 1; i <= N; i++)
        ca[i] = ca[i] + ca[i - 1];
    ll all = 0;
    for (int i = 1; i <= N; i++)
        if (n >= i)
            all = (all + (ca[i] * ca[n - i]) % p) % p;
    cout << (all - same + p) % p << endl;
    return 0;
}

 

 

 

 

 《重点考察化繁为简,对每个值单独考虑求贡献的思路》

《Tokitsukaze and K-Sequence》

 

 

 

 对于这道题,首先就有个想法:

  既然在一个序列中只有出现一次的数才有贡献

  为了使贡献值最大,假设有n个序列,对于同一个数,将其中一个放到一个序列中

  如果还有剩余,那么将其余全部的个数放到最后一序列中

  

  对于同一个数num,假设其有m个

  对于序列只有

  1个时:贡献为0 

  2个时:贡献为1

  3个时:贡献为2

  ....

  m-1个时:贡献为m-2

  m个时:贡献为m

  m+1个时:贡献为m+1

  我们发现:

    当序列个数n,n<m,那么贡献为n-1

    当序列个数n,n==m,那么贡献为n

    当序列个数n,n>m,那么贡献为n

 

于是这道题的方案也就出来了:

  我们首先可以对原序列a,统计其中数num,到底出现了多少次cnt[num]

  同时找到不同的num有多少个:sum

  我们枚举序列个数:i

  对于cnt[num]<=i的  ans+=cnt[num],sum--

  对于剩余cnt[num]>i的  ans+=sum*(i-1)

 

#include <iostream>
#include <algorithm>
#include <cstring>
#include <set>
using namespace std;
const int N = 1e5 + 2;
int cnt[N], n;
void solve()
{
    cin >> n;
    memset(cnt, 0, sizeof(cnt));
    for (int i = 1; i <= n; i++)
    {
        int num;
        scanf("%d", &num);
        cnt[num]++;
    }
    multiset<int> s;
    for (int i = 1; i <= 100000; i++)
        if (cnt[i])
            s.insert(cnt[i]);
    int ans = 0, sum = s.size();
    auto j = s.begin();
    for (int i = 1; i <= n; i++)
    {
        while (j != s.end() && *j <= i)
        {
            ans += *j;
            j++;
            sum--;
        }
        cout << ans + (long long)(i - 1) * sum << endl;
    }
}
int main()
{
    int t;
    cin >> t;
    while (t--)
        solve();
    return 0;
}

 

《Tokitsukaze and Musynx》

 

 

 

   

(我在这里先吐槽一下:这个题目意思也太难看懂了,还是看样例看懂意思的)

 

这道题的题意为:

  划分出5个区间:

  (-INF,a),[a,b),[b,c),[c,d],(d,+INF)

  每个区间有对于的值v1,v2,v3,v4,v5

  给n个音符,开始每个音符在的区间为xi(1<=i<=n)

  我们可以个这n个音符的位置同时+上h

  问全部音符的值的和最大为多少

  

首先暴力想的话只要枚举h这一条道路了吧:

  但是会超时

但是真的有必要每一个h都枚举吗?

  这里我们专门拿出一个音符来分析:

  

 

 

  假设区间为:

    (-INF,1)【1,5)【5,10)【10,16】(16,+INF)

  假设有5个音符,初始位置为:

 

 

 

 

 

  对于第一个音符:

    其初始在区间v2上

    我们想将其移动到区间v1,那么至少要h=-1

    这个时候贡献值的变化为:ans-=v2,ans+=v1

    对于h<=-1,贡献值不再变化

    

    我们想将其移动到v3,那么至少h=+4

    这个时候贡献值的变化为:ans-=v2,ans+=v3

 

    对于4<=h<=8,贡献值不再变化

    ...........

    移动到其余区间也如此分析:

      我们可知:我们并不需要枚举每一个h

           只要枚举使得音符贡献值变化的h即可

           这样的h,对于一个音符来说只有4个

      对于n个音符来说也只有4*n个

 

我们收集起这些h,然后枚举,然后记录贡献值的变化

为了方便操作,可以初始给每一个音符的位置都-=INF,即使他们都有处于v1区间

 

#include <iostream>
#include <cstring>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 2 * 1e5 + 2;
const ll INF = 1e10;
ll arr[N], n, pos[5], vs[6];
void init()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        ll num;
        scanf("%lld", &num);
        arr[i] = num - INF;
    }
    for (int i = 1; i <= 4; i++)
    {
        ll num;
        scanf("%lld", &num);
        if (i == 4)
            num++;
        pos[i] = num;
    }
    for (int i = 1; i <= 5; i++)
        scanf("%lld", &vs[i]);
}
void solve()
{
    init();
    ll ans = vs[1] * n, res = ans;
    map<ll, vector<int>> s;
    for (int i = 1; i <= n; i++)
        // 对于每一个音符单独考虑贡献,这里我们的h并不用全部枚举
        // 而是只要枚举首个能够改变某一个音符的h
        // 这样的h最多有4*n个
        for (int j = 1; j <= 4; j++)
        {
            ll h = pos[j] - arr[i];
            s[h].push_back(j + 1);
            // 这个是需要模拟样例才能明白的操作
            // 简单来说,如从v1->v2,想要迭代ans,我们必须先-vs[1],再+vs[2]
            s[h].push_back(-j);
        }
    for (auto i = s.begin(); i != s.end(); i++)
    {
        vector<int> t = i->second;
        for (int j = 0; j < t.size(); j++)
        {
            if (t[j] > 0)
                ans += vs[t[j]];
            else
                ans -= vs[-t[j]];
        }
        res = max(ans, res);
    }
    cout << res << endl;
}
int main()
{
    int t;
    cin >> t;
    while (t--)
        solve();
    return 0;
}

 

《Tokitsukaze and Gold Coins (easy)》

 

 这道题的难点在于:

  我如何判断某个点上的金币是否拿了没有,如何统计金币?

 

这里给出两种做法:

  1.dfs(y,x):

    表示从点(x,y)能够到达终点,则说明这个点(x,y)处的金币能够拿

    用dfs则要进行剪枝,避免超时

    具体操作是,如果已知某个点能到或者不能到终点,这个时候就可以直接返回了

    不必再搜下去

  

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 5 * 1e5 + 2;
int n, k, st[N][4];
vector<vector<int>> g(N + 1, vector<int>(4));
int dy[] = {0, 1}, dx[] = {1, 0};
int ans;
bool dfs(int y, int x)
{
    if (y == n && x == 3)
        return true;
    if (st[y][x] == 1)
        return true;
    bool flag = false;
    for (int i = 0; i <= 1; i++)
    {
        int ny = y + dy[i], nx = x + dx[i];
        if (ny < 1 || ny > n || nx < 1 || nx > 3)
            continue;
        if (st[ny][nx] == -1 || g[ny][nx])
            continue;
        if (dfs(ny, nx))
        {
            if (st[y][x] == 0)
                ans++;
            st[y][x] = 1;
            flag = true;
        }
    }
    if (!flag)
    {
        st[y][x] = -1;
        return false;
    }
    return true;
}
void solve()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= 3; j++)
        {
            g[i][j] = 0;
            st[i][j] = 0;
        }
    for (int i = 1; i <= k; i++)
    {
        int y, x;
        scanf("%d%d", &y, &x);
        if (g[y][x])
            g[y][x] = 0;
        else
            g[y][x] = 1;
    }
    ans = 0;
    dfs(1, 1);
    cout << ans << endl;
}
int main()
{
    int t;
    cin >> t;
    while (t--)
        solve();
    return 0;
}

 

  2.bfs的写法:

    需要从起始点来一遍bfs到终点,看一下起点到终点会走哪些路

    再从终点到起点再来一遍bfs到起始点,看一下终点到起点会走哪些路

    两边bfs重叠走的路,就是正确的路,这些路上的金币都拿下

 

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 5 * 1e5 + 2;
int n, k;
bool st[N][4][2];
vector<vector<int>> g(N + 1, vector<int>(4));
int dy[] = {0, 1}, dx[] = {1, 0};
int by[] = {-1, 0}, bx[] = {0, -1};
void bfs(int y, int x, int s)
{
    queue<PII> q;
    q.push({y, x});
    while (q.size())
    {
        PII t = q.front();
        q.pop();
        st[t.first][t.second][s] = true;
        for (int i = 0; i < 2; i++)
        {
            int ny, nx;
            if (s == 0)
                ny = t.first + dy[i], nx = t.second + dx[i];
            else
                ny = t.first + by[i], nx = t.second + bx[i];
            if (ny < 1 || ny > n || nx < 1 || nx > 3)
                continue;
            if (st[ny][nx][s] || g[ny][nx])
                continue;
            q.push({ny, nx});
        }
    }
}
void solve()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= 3; j++)
        {
            g[i][j] = 0;
            for (int k = 0; k <= 1; k++)
                st[i][j][k] = false;
        }
    for (int i = 1; i <= k; i++)
    {
        int y, x;
        scanf("%d%d", &y, &x);
        if (g[y][x])
            g[y][x] = 0;
        else
            g[y][x] = 1;
    }
    bfs(1, 1, 0);
    bfs(n, 3, 1);
    int ans = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= 3; j++)
        {
            if (st[i][j][0] == st[i][j][1] && st[i][j][0])
                ans++;
        }
    if (ans > 0)
        cout << ans - 1 << endl;
    else
        cout << ans << endl;
}
int main()
{
    int t;
    cin >> t;
    while (t--)
        solve();
    return 0;
}

 

 

 

 《Tokitsukaze and Function》

 

 

这道题细节很多,要小心:

  设g(x)=n/x+x

  设f*(x)=

 

 

  

 

 

    f*(x)<=g(x)

  g(x)-f*(x)<=1

  可以猜想出f*(x)的图像是在g(x)图像下方,

  十分贴近g(x)的不连续的点图(因为f*(x)的x只取整正数)

  

  但是f*(x)的最小值点也一定还是在x=sqrt(x)的附近,

  同理f(x)的最小值点也一定还是在x=sqrt(x)的附近,

  我们在确定最小值后,为了寻求最小的x使得f(x)最小

  我们可以对最小值点r=minx,l=l

  进行二分

  注意minx不在【l,r】的情况

 

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
void solve()
{
    ll n, l, r;
    cin >> n >> l >> r;
    ll minx = sqrt(n);
    if (minx < l || minx > r)
    {
        ll ansl = n / l + l - 1, ansr = n / r + r - 1;
        if (ansl <= ansr)
            cout << l << endl;
        else
        {
            // 注意在r这里也要找到最小的对应的下标值
            ll el = l, er = r, ans;
            while (el <= er)
            {
                ll mid = (el + er) >> 1;
                if (n / mid + mid - 1 <= ansr)
                {
                    ans = mid;
                    er = mid - 1;
                }
                else
                    el = mid + 1;
            }
            cout << ans << endl;
        }
        return;
    }
    ll pos0 = max(l, (ll)sqrt(n) - 1), pos1 = sqrt(n), pos2 = min(r, (ll)sqrt(n) + 1);
    ll ans0 = n / pos0 + pos0 - 1, ans1 = n / pos1 + pos1 - 1, ans2 = n / pos2 + pos2 - 1;
    ll el = l, er, ans, stand;
    if (ans0 <= ans1 && ans0 <= ans2)
        er = pos0, stand = ans0;
    else if (ans1 <= ans0 && ans1 <= ans2)
        er = pos1, stand = ans1;
    else if (ans2 <= ans0 && ans2 <= ans1)
        er = pos2, stand = ans2;
    while (el <= er)
    {
        ll mid = (el + er) >> 1;
        if (n / mid + mid - 1 <= stand)
        {
            ans = mid;
            er = mid - 1;
        }
        else
            el = mid + 1;
    }
    cout << ans << endl;
}
int main()
{
    int t;
    cin >> t;
    while (t--)
        solve();
    return 0;
}

 

  

 

posted @ 2023-01-29 13:12  次林梦叶  阅读(95)  评论(0)    收藏  举报