CF 2127

C:


题目分析

首先可以发现,Ali每开启一轮游戏之后,需要花的钱只增不减。

又因为每次选择的 \(i\)\(j\) 是任意的,所以可以反复选择同一组 \(i\)\(j\),这样需要花的钱的增加量一定最小。

所以可以发现这个 \(k\) 给的挺多余的。

解题思路

那么我们的任务就是找到一组 \(i\)\(j\),使这组经过交换后的所需钱的增长量最少。

可以发现,当一对满足 \(a_i \le a_j\)\(i\)\(j\) 满足 \(b_i \ge a_j\) 时,经过任何交换需要花的钱都不会增长。那么我们把每一组 \(a_i,b_i\)\(a_i\) 为关键字进行排序,如果找到一组满足上述要求的 \(i\)\(j\),就能直接输出初始所需要的钱。

如果没有找到那样一组,那么对于任意一组满足 \(a_i \le a_j\)\(i\)\(j\),有 \(b_i<a_j\)。容易发现,对于这样的一组,交换后增加的钱数为 \(2*(a_j-b_i)\)。如图:

(图片太大了碍事)

那么我们只需要求一组 \(a_j-b_i\) 最小的就可以了。

参考代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
int t;
int n,k;
const int N=2e5+10;
struct node
{
    int a,b;
    bool operator<(const node &wow) const
    {
        return (a==wow.a) ? b<wow.b : a<wow.a;
    }
}wow[N];

signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>t;
    while(t--)
    {
        cin>>n>>k;
        for(int i=1;i<=n;i++)
        {
            cin>>wow[i].a;
        }
        for(int i=1;i<=n;i++)
        {
            cin>>wow[i].b;
        }

        for(int i=1;i<=n;i++)
        {
            if(wow[i].a>wow[i].b)
            {
                int w=wow[i].a;
                wow[i].a=wow[i].b;
                wow[i].b=w;
            }
        }
        sort(wow+1,wow+n+1);
        
        bool flag=1;
        for(int i=1;i<n;i++)
        {
            if(wow[i+1].a<wow[i].b)
            {
                flag=0;
            }
        }
        int sum=0;
        for(int i=1;i<=n;i++)
        {
            sum+=wow[i].b-wow[i].a;
        }
        if(flag==1)
        {
            int minn=INT_MAX;
            for(int i=1;i<n;i++)
            {
                minn=min(minn,wow[i+1].a-wow[i].b);
            }
            cout<<sum+2*minn<<'\n';
        }
        else
        {
            cout<<sum<<'\n';
        }

    }
    return 0;
}



D:


——根因爱而生,因命运而断——

尝试新思路,一个码风非常漂亮的dfs,假了。
入类错误思路大赏,我的意思是你可以展开以下内容观察一下非碳基动物的思维方式

点击查看代码
因为桥不能交叉,所以每一个节点只能有两个非叶子节点的孩子,且一个放在最左边,一个放在最右面。如果这一点不能保证,那么可以输出 $0$。

还可以发现把非叶子节点的孩子放在左右两边的结果是对称的,所以我们只需要算其中一边。

以及剩余 $x$ 个叶子子节点可以任意排列,贡献为 $A^x_x$。

那么这个节点的贡献为 $A^x_x$ $*$ $(非叶子节点的贡献(如果有的话,没有的话记非叶子节点贡献为 \frac{1}{2}) *2)$。

最后的答案就是根节点的贡献辣。

这种思路会被如 1-2 2-3 2-4 这种数据卡掉

因为上述思路成立的条件是被放在某一侧的非叶子节点的儿子们一定在它父亲的同侧。而事实不然。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int t,n,m;
#define int long long
const int N=2e5+10,mod=1e9+7;
vector<int> vec[N];//存儿子们的
int d[N],val[N],fa[N];//记录每个节点的度数,贡献,父节点
int son[N];//非叶子子节点个数
int A[N];//排列数

inline void init()
{   
    A[0]=A[1]=1;
    for(int i=2;i<N;i++) A[i]=1ll*A[i-1]*i,A[i]%=mod;  
}

void solve(int u,int father)//当前需要统计贡献的节点
{
    fa[u]=father;
    if(d[u]==1 && u!=1) return ;//遍历到叶节点  

    son[father]++;
    for(int x : vec[u])
    {
        if(x==fa[u]) continue;
        solve(x,u);
        val[u]*=1ll*val[x];
        val[u]%=mod;
    }
    int num=d[u]-son[u]-1;//叶子节点的个数
    if(u==1) num++;//根节点
    val[u]*=1ll*A[num];
    val[u]%=mod;
    
}

inline bool check()
{
    if(n!=m+1) return false;
    //for(int i=1;i<=n;i++) cout<<d[i]<<" ";
    solve(1,0);

    for(int i=1;i<=n;i++)
    {
        if(son[i]>1 && i!=1) return false;
        if(i==1 && son[i]>2) return false;
    }
    for(int i=1;i<=n;i++) 
        if(son[i]!=0)
        {
            val[1]*=2ll,val[i]%=mod;//先按照合法的算
            break;
        }
    cout<<val[1]*2ll%mod<<'\n';
    return true;
}

signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>t;
    init();
    while(t--)
    {
        cin>>n>>m;
        for(int i=1;i<=n;i++) d[i]=0,val[i]=1,son[i]=0,vec[i].clear();
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            vec[u].push_back(v),vec[v].push_back(u);
            d[u]++,d[v]++;
        }
        if(!check()) cout<<"0\n";
        //for(int i=1;i<=n;i++) cout<<son[i]<<' ';
    }
    return 0;
}

题目分析

数数题。首先让我们来看看合法的情况。可以发现,合法的情况一定是一棵树。

解题思路

讲正解。首先是无解的条件,很好想。

  1. 如果图非树(有环)则必定有桥交叉。
  2. 所有非叶子节点组成了一条链。否则桥必定交叉。

链上每个节点上连的 \(x\) 个叶子节点可以自由排列,贡献为 \(A^x_x\)。记这个值为链上非叶子节点的贡献。

我们要求的结果就是链上所有节点贡献的乘积。

最后,当链上的节点数量大于 \(1\) 的时候,我们可以把链以垂直于河直线为对称中心翻转,所以 \(ans*2\)。无论什么情况,我们可以交换河两边的屋子,所以 \(ans*2\)。做完了。

参考代码

zro 一腔愤怒的结晶

#include<bits/stdc++.h>
using namespace std;
#define ll long long
//听老师的话,别用 #def int ll
const int N=2e5+10,mod=1e9+7;
int t,m,n;
ll A[N];
bool adm[N];//是否在主链上
vector<int> vec[N];//存边)

void init()//排列数
{
    A[0]=A[1]=1;
    for(int i=2;i<N;i++) A[i]=A[i-1]*i*1ll,A[i]%=mod;
}

int check(int u) //看看与这个非叶子节点相连的非叶子有几个
{
    int num=0;
    for(int x : vec[u]) if(adm[x]) num++;
    return num;
}

void Cathyzro()
{
    cin>>t;
    init();
    while(t--)
    {
        cin>>n>>m;
        
        for(int i=1;i<=n;i++) vec[i].clear(),adm[i]=0;
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            vec[u].push_back(v),vec[v].push_back(u);
        }

        if(n!=m+1) //非树
        {
            cout<<"0\n";
            continue;
        }
        
        for(int i=1;i<=n;i++) if(vec[i].size()>1) adm[i]=1; //判点是否非叶子
        
        ll ans=1,cnt=0;
        for(int i=1;i<=n;i++) if(adm[i]) ans*=A[vec[i].size()-check(i)]*1ll,ans%=mod,cnt++; //统计答案

        bool flag=1;
        for(int i=1;i<=n;i++) if(check(i)>2){ cout<<"0\n"; flag=0; break; } //非叶子是否成链

        if(cnt>1) ans*=2ll,ans%=mod; //链长>1

        if(flag) cout<<ans*2ll%mod<<'\n';
    }
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    Cathyzro();

    return 0;
}


E:

前言: 大佬一句 E<<D,小蒟蒻 zro 调了一天又一天。

题目分析

当一个节点可爱的时候满足:

  • \(v\) 节点为 LCA 的两个节点 \(i,j\)\(c_i = c_j\)
  • \(c_v \ne c_i\)

所以当我们发现有两个 \(c\) 相同的节点 \(i,j\) 时,我们应使其 LCA 等于 \(c_i\)

解题思路

看到了“具有相同属性的一类点的 LCA”这种思路,想到虚树

我们把每种颜色(不包括无色)的节点当作关键点,建一棵虚树。特别的,这里的虚树只需插入关键点的 LCA 们即可。因为关键点本身一定不是 cutie 点,也不需要你染色。

我们对节点进行分类讨论。

  1. 该节点不在任何一棵虚树上
  2. 该节点在一棵虚树上
  3. 该节点在大于两棵虚树上

首先可以发现,如果一个点在虚树上且它已经有颜色了,如果它的颜色和所在虚树的颜色不一样,那么这个节点一定很 cutie

对于第一种情况,可以保证该节点不是 cutie。如果该节点需要染色呢?

  • 如果以这个节点为根节点的子树中全部未被染色,那么选取它的父亲节点的颜色,因为这样可以保证它的父亲不会因为它这个决策变成 cutie 且因为子树中没有任何颜色所以它一定不会变成 cutie
  • 如果这个节点为跟节点的子树中存在某种颜色,那么选取这个颜色,这样可以保证该节点不被子树上节点变成 cutie,且在子树外的情况,该节点和它子树上的节点与另一个子树外的节点的 LCA 使相同的,所以不会额外增加代价。

对于第二种情况,用它所在虚树的颜色为它着色(如果它没在一开始就被判断为 cutie 的话。

对于第三种情况,它一定很 cutie,用它所在任意虚树的颜色为它着色以保证它不会额外增加代价。

我觉得如果想写对的话还是要注意一下各种情况处理的时机。比如用虚树上的颜色更新颜色就在建虚树的时候直接做完。小蒟蒻就是先建完虚树再上的色,然后在一片混乱中被迫重构代码的(小声)。

参考代码

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
/*
变量含义详解:
t,n,k,w[],c[]如题
vec[]存边的
dep[],sz[],top,son[],fa[] 树剖求LCA的坤本数组(数组名甚至很美观)
mark[]下文解释了,虚树去重用的
cutie[]似乎可以开bool,一个点是不是cutie
cnt[]一个点出现在几个虚树中
col[]最终这个点被染成的颜色
dfn[]dfs序,虚树里用的
vt[]装关键点的,该vector第一维表示颜色
vt[]装虚树中的点的,第一维同上
ct用来求dfs序(dfn[])

*/
int t,n,k;

int w[N],c[N];
vector<int> vec[N];

int dep[N],sz[N],top[N],son[N],fa[N];
int mark[N],cutie[N],cnt[N],col[N];
int dfn[N];
vector<int> vt[N],vt2[N];//一堆虚树
int ct=0;

//以下三个函数为树剖求LCA。
void dfs1(int u,int father)
{
    dep[u]=dep[father]+1,fa[u]=father,sz[u]=1,dfn[u]=++ct;
    for(int x : vec[u])
    {
        if(x==fa[u]) continue;
        dfs1(x,u);
        if(sz[x]>sz[son[u]]) son[u]=x;
        sz[u]+=sz[x];
    }
}

void dfs2(int u,int t)
{
    top[u]=t;
    if(son[u]) dfs2(son[u],t);
    for(int x : vec[u])
    {
        if(x==son[u] || x==fa[u]) continue;
        dfs2(x,x);
    }
}

int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    if(dep[u]>dep[v]) swap(u,v);
    return u;
}


//这里将没染色的点染上父亲的颜色,染色策略在解题思路中
void draw(int u,int coll)
{
    if(c[u]) coll=c[u];
    else c[u]=coll;
    for(int x : vec[u]) if(x!=fa[u]) draw(x,coll);
}

//很典的建虚树呐。
inline void vitual_tree()
{
    for(int i=1;i<=k;i++)
    {
        sort(vt[i].begin(),vt[i].end(),[&](int u,int v){return dfn[u]<dfn[v];});//很新奇的语法,用来vector中一层内排序

        for(int j=0,lcaa;j+1<vt[i].size();j++)
        {
            lcaa=lca(vt[i][j],vt[i][j+1]);
            if(!mark[lcaa]) 
            {
                if(c[lcaa]) 
                {
                    if(c[lcaa]!=i) cutie[lcaa]=1;//对应第二种
                    continue;
                }
            mark[lcaa]=1;//防止重复,相当于虚树板子里后来的unique
            cnt[lcaa]++;
            col[lcaa]=i;
            vt2[i].push_back(lcaa);
            }
        }
        for(int v : vt2[i]) mark[v]=0; //别忘了清空哦~
        vt[i].clear();
    }
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>t;
    while(t--)
    {
        cin>>n>>k;
        //初始化
        ct=0;
        for(int i=1;i<=n;i++) dfn[i]=0,vec[i].clear(),mark[i]=0,cutie[i]=0,cnt[i]=0,vt[i].clear(),vt2[i].clear(),dep[i]=0,son[i]=0,fa[i]=0,top[i]=0,sz[i]=0,col[i]=0;

        for(int i=1;i<=n;i++) cin>>w[i];
        for(int i=1;i<=n;i++) cin>>c[i], vt[c[i]].push_back(i);
        for(int i=1,u,v;i<n;i++) cin>>u>>v, vec[u].push_back(v), vec[v].push_back(u);

        bool flag=0;
        for(int i=1;i<=n;i++) flag|=c[i];
        if(!flag)
        {
            cout<<"0\n";
            for(int i=1;i<=n;i++) cout<<"1 ";
            cout<<'\n';
            continue;
        }//这一段在判断是否为全0随便染色

        dfs1(1,0),dfs2(1,1);
        
        vitual_tree();

        long long val=0;
        for(int i=1;i<=n;i++)
        {
            if(cutie[i] || cnt[i]>1) val+=w[i];//之前确认的cutie和第三种情况
            if(!c[i] && cnt[i]==1) c[i]=col[i];//第二种情况,如果可着色,那么着色
        }
        cout<<val<<"\n";

        for(int i=1;i<=n;i++)
            if(c[i])
            {
                draw(1,c[i]);
                break;
            }
        
        for(int i=1;i<=n;i++) cout<<c[i]<<' ';
        cout<<'\n';
    }
    return 0;
}

其他做法

除此之外,还可以使用树上启发式合并统计在它的两个以上子树中出现的节点的集合。

与这里讲述思路相同的是,该做法也需要统计集合中元素的数量,类似于该节点在几棵虚树上。选色思路也与这里讲述的思路相同。

posted @ 2025-08-08 11:12  cathyzro  阅读(29)  评论(0)    收藏  举报