The 2021 China Collegiate Programming Contest (Harbin)-2021 CCPC 哈尔滨(7/13)

Dashboard - The 2021 China Collegiate Programming Contest (Harbin) - Codeforces
 

 B. Magical Subsequence 线性二维DP

C. Colorful Tree 树上启发式合并,multiset维护,贪心

D. Math master 二进制枚举,模拟,细节

E. Power and Modulo 思维,模拟

G. Damaged Bicycle 图论,状压,记忆化,期望,数学

I. Power and Zero 思维,模拟,进制

J. Local Minimum 签到

 Magical Subsequence

dp[i][j]代表 前i个数字中,两两和为j的序列长度

dp[i][j]=dp[pre][j]+2  pre=last[i][j-a[i]]-1    即选了当前a[i]对应和为j的数字上一次出现的位置

#include 
using namespace std;
typedef long long ll;
const int N=1e5+5;
int dp[100000+10][210];
int a[100000+10];
int pre[100000+10][210];
int main()
{

    int n;
    cin>>n;

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

        for(int j=1; j<=100; j++)
        {
            if(a[i]==j)
            {
                pre[i+1][j]=i;
            }
            else
            {
                pre[i+1][j]=pre[i][j];
            }
        }
    }


    int ans=0;
    for(int i=1; i<=n; i++)
    {
        for(int j=1; j<=200; j++)
        {

            dp[i][j]=dp[i-1][j];
            int fuck=j-a[i];

            if(fuck<=0)
                continue;

            if(pre[i][fuck])
            {
                int pos=pre[i][fuck];
                dp[i][j]=max(dp[i][j],dp[pos-1][j]+2);
            }
            ans=max(ans,dp[i][j]);
        }
    }

    cout<

 C. Colorful Tree

树形dp开二维来写的话,就是dp[i][j]代表i子树本次染色j的最小代价,当然染色是自上而下的,dp是自下而上的。这样一定会T,然后我们发现,儿子是若干叶子结点的时候,当前节点一定是染成颜色最多的那个。再往上一层,可以把本层也看做叶子,其颜色就是这次染成的颜色。但是我们发现,虽然选择染颜色的众数比非众数更优,但众数与众数之间是存在优劣的,并且这种优劣我们无法预估。

考虑每次把当前节点全部儿子染色的众数放在一个集合,把他们看成新的叶子,选择一个即可。启发式合并,不对重儿子遍历,只把轻儿子合并到重儿子。

比如4号节点管辖了5个叶子,2个红色,2个绿色,一个蓝色,我们当前自然是选择红绿都行,那么就都放进4号点的multiset,其dp值就代表4号点选择红或者绿时的dp值,也就是说,我们获得了dp值的共享。 假如4号点的兄弟节点5有2个红色,那么在统计4,5共同父亲3号点的时候,显然我们选红色最优。利用启发式合并直接把5,4的multiset合并,就能获得红色次数为4。这一过程,本质上是3号点先把全部叶子染成红色,5号不用染,4号只需一个蓝色

dp[i] = sum dp[son] - maxx+1  也就是当前染一次,抵消了儿子若干次。

但这样做仍然会T,启发式合并不能遍历重儿子,但我们还是遍历了。即当maxx(众数的出现次数)=1的时候,退化为n^2 .所以只在maxx>1的时候进行继承,因为我们无论当前众数每个出现几次,最终我们存入multiset的只是众数的个数,这本身也是一种合并,且数量最少减少二倍。

#include

using namespace std;

vectorv[100000+10];
int col[100000+10],sizeson[100000+10],dp[100000+10];
multisets[200000+10];
int tot;
void dfs(int now,int pre)
{
    if(v[now].size()==0)
    {
        sizeson[now]=1;
        dp[now]=1;
        s[now].insert(col[now]);
        return ;
    }
    int maxx=1;
    int cnt=0;
    for(auto it:v[now])
    {
        if(it==pre)
            continue;
        dfs(it,now);
        dp[now]+=dp[it];
        if(s[now].size()1)
    {
        multisetss;
        for(auto it:s[now])
        {
            if((int)s[now].count(it)==maxx&&!ss.count(it))
                ss.insert(it);
        }
        swap(s[now],ss);
    }
    dp[now]=dp[now]-maxx+1;
}

int main()
{
    int n;
    cin>>n;

    for(int i=2; i<=n; i++)
    {
        int x;
        cin>>x;
        v[x].push_back(i);
    }

    for(int i=1; i<=n; i++)
    {
        cin>>col[i];
    }
    tot=n;
    dfs(1,0);
    cout<

 D. Math master

本题细节很多,是个比较容易罚时的模拟,技巧性挺高。

注意,本题删除数字后的前导0不会自己消失。

首先为了保持最终比值不变,我们最好求出分子分母化简之后的结果。

我们暴力枚举分子各个数字的去除情况,利用当前数字,和分子分母化简之后的比值,求出对应分母,检测分母合法性,更新答案即可。 

细节一,分子分母不事先化简会爆ll

细节二,删除分子的时候,删除每个数字的个数不能超过该数字在分子分母出现次数的min

细节三,分子不能为0

细节四,分母很有可能具有前导0

细节五,根据分子删除数字个数得出分母删除个数x,len分母-x>当前求出分母长度时,不合法

细节六,len分母-x是理论上,不具备前导0的时候,分母该有的长度,当前分母如果长度<这一长度,说明必然具有前导零。

细节七,把当前分母各个位置与原分母一一按照顺序匹配,不能匹配成功则不合法

细节八,匹配完成后,仍需比较各个数字与当前a删除个数是否相同,与数字次数min大小关系。

#include
using namespace std;
typedef long long ll;
using namespace std;
ll a[100],b[100],lena,lenb;
ll n,m;
ll mincnt[30],cnta[30],cntb[30],tempcnt[30];
ll checkcnt[30];
void init()
{
    lena=lenb=0;
    ll temp=n;
    memset(cnta,0,sizeof(cnta));
    memset(cntb,0,sizeof(cntb));
    memset(mincnt,0,sizeof(mincnt));
    while(temp)
    {
        a[lena]=temp%10;
        cnta[temp%10]++;
        lena++;
        temp/=10;
    }
    temp=m;
    while(temp)
    {
        b[lenb]=temp%10;
        cntb[temp%10]++;
        lenb++;
        temp/=10;
    }
    for(int i=0; i<=9; i++)
    {
        mincnt[i]=min(cnta[i],cntb[i]);
    }
}
int tempb[20];
bool check(ll fuckb,int nowcnt)
{

    //cout<lenb-nowcnt)
        return 0;

    lenn=lenb-nowcnt;
    int bb=0;

    int cntt=0;
    for(int i=0; imincnt[i])
            return 0;
    }
    return 1;
}
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        ll gcd=__gcd(n,m);
        ll nn=n/gcd,mm=m/gcd;
        init();
        ll ansa=1e19,ansb=1e19;
        for(int i=1; i<(1<mincnt[a[j]])
                    {
                        flag=1;
                        break;
                    }
                }
            }

            if(flag)
                continue;
            ll fuckb=now/nn*mm;
            if(now%nn||now==0)
            {
                continue;
            }
            else
            {
                if(check(fuckb,nowcnt))
                {
                    if(now

E. Power and Modulo

是个思维题,一段连续二倍递增的,一定是

一旦下降,这个值就被唯一确定,因为m>2^i 但m<=2^(i+1),m的位置在区间只有一个。

然后就检验后面合法性就够了

#include 
using namespace std;
typedef long long ll;
int pw(ll a, int b, int p)
{
    a %= p;
    ll res = 1;
    while (b)
    {
        if (b & 1)
            res = (res * a) % p;
        a = a * a % p;
        b >>= 1;
    }
    return (res % p + p) % p;
}
int main()
{
    int t;
    scanf("%d", &t);
    while (t--)
    {
        int n;
        scanf("%d", &n);
        vector a(n + 1);
        ll ans = 1, idx = 1;
        bool f = false, flag = false;
        for (int i = 1; i <= n; i++)
        {
            scanf("%d", &a[i]);
            if (a[i])
                flag = true;
            if (!f && a[i] != (1ll << (i - 1)))
            {
                f = true;
                idx = i;
                ans = (1ll << (i - 1)) - a[i];
            }
        }
        if (!flag)
        {
            cout << 1 << "\n";
            continue;
        }
        bool ok = 1;
        for (int i = idx; i <= n; i++)
        {
            if (pw(2, i - 1, ans) != a[i])
            {
                ok = 0;
                break;
            }
        }
        if (ok)
        {
            cout << ans << "\n";
        }
        else
            cout << "-1\n";
    }
    return 0;
}

G. Damaged Bicycle

k<=18联系到状压。

分类讨论,一号点只有两种情况,一个是非自行车点,一个是自行车点

第一种情况,又分为两种,直接到n,这一期望就是距离值,一个是途径某个自行车点到n

第一种情况的第二种,即1号点最短路到某个自行车点,到达之后,又分为两种,即直接成功或者到下一个。。这一依次循环

对于1号点是自行车点的情况,与第一个类似。

考虑记忆化,dp[st][now]状态st的时候,走到now时,到达n的期望时间。0代表没有走到,1代表之前走到。值得注意的是,因为我们骑到自行车就一定直接骑到终点,所以我们当前状态中有1的,一定是被鉴定为损坏的,是坚决不能再走的,如果走,会将其又当做好的自行车来算。 

提前跑18次最短路预处理一下最短路径。

先假设1号点不是自行车点,如果是,答案也会被覆盖。

#include 
using namespace std;

typedef long long int ll;

double dis[20][100000+10],inf=1e18;
int book[100000+10];
typedef struct
{
    int b,e;
    ll val;
}xinxi;
xinxi s[1000000+10];
int f[1000000+10],nex[1000000+10],len;
ll bu,che;
int n,m;
void add(int x,int y,ll z)
{
    s[len].b=x;
    s[len].e=y;
    s[len].val=z;
    nex[len]=f[x];
    f[x]=len;
    len++;
}
int a[1000];
struct node
{
    int id;
    ll dis;
    friend bool operator<(node x, node y)
    {
        return x.dis>y.dis;
    }
};
priority_queueq;

void work(int st,int pos)
{

    for(int i=1;i<=n;i++)
    {
        dis[pos][i]=1e18;
        book[i]=0;
    }
    dis[pos][st]=0;
    struct node head;
    head.id=st;
    head.dis=dis[pos][st];
    q.push(head);
    while(!q.empty())
    {
        struct node now=q.top();
        q.pop();
        if(book[now.id])
            continue;
        book[now.id]=1;
        int x=f[now.id];
        while(x!=-1)
        {
            int j=s[x].e;
            if(dis[pos][j]>dis[pos][now.id]+s[x].val)
            {
                dis[pos][j]=dis[pos][now.id]+s[x].val;
                struct node tail;
                tail.dis=dis[pos][j];
                tail.id=j;
                q.push(tail);
            }

            x=nex[x];
        }
    }

}
double p[100];
double dp[(1<<18)][20];
int vis[(1<<18)][20];
int tot;
double dfs(int staus,int now)
{

    if(vis[staus][now])
        return dp[staus][now];
    vis[staus][now]=1;
    double nowans=(1-p[now])*((double)dis[now][n]/che)+(p[now])*((double)dis[now][n]/bu);

    for(int i=0;i>bu>>che;
    cin>>n>>m;
    memset(f,-1,sizeof(f));
    for(int i=1;i<=m;i++)
    {
        int x,y;
        ll z;
        cin>>x>>y>>z;
        add(x,y,z);
        add(y,x,z);
    }

    cin>>tot;
    for(int i=0;i>a[i]>>p[i];
        p[i]/=100;
        work(a[i],i);
    }

    work(1,tot);

    if(dis[tot][n]>=1e17)
    {
        cout<<-1<

I. Power and Zero

贪心的想,一定是每次从1开始删一段最长的连续的。

1 1  0  0 0  1  1 (右小) 这种,我们删除,1,2,4,8,16,其中4,8,16全部被32吸收,他会吐出来32-4-8-16=4 ,也就是右侧第一个0,这样我们发现,这个过程就是向高位借1的。

我们从第一位开始推,有就--,否则找最前面一个1,位置pos,把pos-1到--当前位置全部设为1

,注意当前位置不要删除这个借来的1,然后继续往前推,之前借到的1,也会被依次删除。

#include 
using namespace std;
typedef long long ll;
const int N = 40;
int c[N];
bool chk()
{
    for (int i = 0; i < 35; i++)
    {
        if (c[i])
        {
            return 0;
        }
    }
    return 1;
}
int f(int k)
{
    for (int i = k + 1; i < 35; i++)
    {
        if (c[i])
        {
            return i;
        }
    }
    return -1;
}
int main()
{
    int t;
    scanf("%d", &t);
    while (t--)
    {
        int n;
        scanf("%d", &n);
        memset(c, 0, sizeof c);
        for (int i = 0; i < n; i++)
        {
            ll x;
            scanf("%lld", &x);
            for (int j = 0; j < 35; j++)
            {
                if (((ll)1 << j) & x)
                {
                    c[j]++;
                }
            }
        }
        int res = 0;
        while (chk() == 0)
        {
            // pp();
            res++;
            for (int i = 0; i < 33; i++)
            {
                if (c[i])
                {
                    c[i]--;
                }
                else
                {
                    int p = f(i);
                    // printf("%d!!", p);
                    if (p != -1)
                    {
                        for (int k = i; k < p; k++)
                        {
                            c[k] = 1;
                        }
                        c[p]--;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
        printf("%d\n", res);
    }
}

J. Local Minimum

签到

#include 
using namespace std;
typedef long long ll;
const int N = 1e3 + 7;
int q[N][N];
bool a[N][N], b[N][N];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &q[i][j]);
        }
    }
    for (int i = 1; i <= n; i++)
    {
        int mi = 1e9;
        for (int j = 1; j <= m; j++)
        {
            mi = min(mi, q[i][j]);
        }
        for (int j = 1; j <= m; j++)
        {
            if (mi == q[i][j])
            {
                a[i][j] = 1;
            }
        }
    }
    for (int j = 1; j <= m; j++)
    {
        int mi = 1e9;
        for (int i = 1; i <= n; i++)
        {
            mi = min(mi, q[i][j]);
        }
        for (int i = 1; i <= n; i++)
        {
            if (mi == q[i][j])
            {
                b[i][j] = 1;
            }
        }
    }
    int res = 0;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            res += (a[i][j] && b[i][j]);
        }
    }
    printf("%d\n", res);
}

 

你可能感兴趣的:(ICPC区域赛真题,算法,ICPC)