纪中集训8/14题目泛做

30 篇文章 1 订阅
23 篇文章 0 订阅

今天的题目太水了,但是我没有AK
【A T1:亲戚(Relatives)】
本质上是求一种对树的拓扑排序序列方案数问题,形式化地说,就是给出 m 个限制pi<pj(这里 pi 表示 i 在序列中的位置),问长度为n的排列的方案数。
题目中给的是若干森林然后排列,但是我们直接用0来做一个超级根,就变成树上DP的问题了。我们现在考虑对于一个根,该怎么合并他的儿子。
我们先设 f(x) 表示以 x 为根的子树里排列的方案数,ci为其的若干孩子,那么我们先用DFS算出各棵子树的方案数,然后我们继续考虑下 f(c1),f(c2),f(ck) 该如何推出 f(x) 。我们考虑我们将子树的答案不断合并,比如说我们现在要合并 f(ca) f(cb) ,那么很显然,由乘法原理,我们可以得到 ans=f(ca)f(cb)S ,这里我们可以看出 S 相当于我们不考虑ca cb 对应的序列的内部顺序(即相当于两种分别有 size(ca) size(cb) 的物品),而进行排列,那么这是一个经典模型,就是相当于在无区别的盒子(在这里表现为序列间的空隔)里放无区别的球,我们只要考虑在 size(ca)+size(cb) 个物品中选取 size(cb) 个(其实 size(ca) 个也可以,看合并顺序随便写),剩下的自然全部是另一种 ca 对应的物品,那么我们可以得到

ans=f(ca)f(cb)C(size(ca)+size(cb),size(ca))
稍微推广一下,我们就有
f(x)=f(c1)i=2kf(ci)C(size(ci)+size(ci1),size(ci))
代码就很好写了,只要先把阶乘预处理下就好了,然后DFS的时候直接树形DP,如果顺手线性推了逆元,可以是 O(n+m) 的复杂度。

# include <cstdlib>
# include <cstdio>
using namespace std;
const int mod = 1000000007;
const int maxn = 200010;
struct Node{
    Node* next;
    int v;
    Node(){ next = NULL; }
};

Node* G[maxn];

void insert(int u,int v){
    Node* t = new Node();
    t->v = v;
    t-> next = G[u];
    G[u] = t;
}

long long multi[maxn*2];
int n;
int fa[maxn];
long long f[maxn];
int size[maxn];


long long mod_pow(int x,int k){
    if (k == 0) return 1; if (k == 1) return x;
    long long t = mod_pow(x,(k>>1));
    if ((k&1) == 0) return (t*t)%mod;
    else return ((t*t)%mod)*x%mod;
}
inline long long inv(long long x){ return mod_pow(x,mod-2); }

void DFS(int x){
    if (G[x]==NULL){
        f[x] = 1;
        size[x] = 1;
        return;
    }
    DFS(G[x]->v);
    f[x] = f[G[x]->v];
    size[x] = size[G[x]->v];
    for (Node* it=G[x]->next;it!=NULL;it=it->next){
        DFS(it->v);
        f[x] = ((multi[size[x]+size[it->v]]*inv(multi[size[it->v]]*multi[size[x]]%mod))%mod*f[it->v])%mod*f[x]%mod;
        size[x] += size[it->v];
    }
    ++size[x];
}

int main(){
    scanf("%d",&n);
    multi[0] = 1;
    for (int i=1;i<=n*2+1;++i) multi[i] = multi[i-1] * i % mod;
    for (int i=1;i<=n;++i){
        scanf("%d",fa+i);
        insert(fa[i],i);
    }
    ++n;
    DFS(0);
//  for (int i=0;i<n;++i) printf("%d ",f[i]);
//  printf("\n");
    printf("%d\n",f[0]);
    return 0;
}

【A T2 数组(Array)】
其实就是给出一个集合 S ,可以对任意aS执行共 k 次操作,每次变成a+x ax ,使得最终 M=dSd 最小。
把样例推了出来之后,我就发现似乎可以贪心做。我们考虑到,不管这里的乘积是正还是负,绝对值最小的那个 a 就可以使得答案的变化最大,因为改某个数d对于答案变化的贡献是 |Mxd×sgn| ,显然 x 可以转成正数(否则,我们可以考虑下对称性取绝对值,答案仍然一样),符号函数sgn可以直接略去,那么就显然有上面的结论。然后就很容易维护了,我们每次维护下绝对值最小的那个值,并取能够使答案进一步变小的那一个方向操作。具体来说,就是如果当前 M0 ,那么就把 aax ;如果当前 M<0 ,那么就把 aa+x 。但是我们是否要记下 M 呢?并不用,我们只要记下M的正负性就行了,这很容易维护,只要每次改值的时候判断下是否跨越了象限就好了。对于绝对值的最小值,我们用优先队列就可以了,关键是这题要注意开long long int,否则太容易爆掉了。

# include <algorithm>
# include <cstdio>
# include <queue>
using namespace std;
const int mod = 1000000007;
priority_queue<long long,vector<long long>,greater<long long> > Q;
int n,k,p;
long long x;
long long ans=1;
bool isBelow0=false;
bool is0 = false;
int main(){
    //freopen("array.out","w",stdout);
    scanf("%d%d%d",&n,&k,&p);
    p = abs(p);
    for (int i=0;i<n;++i){
        scanf("%lld",&x);
        if (x > 0){ Q.push(x); }
        else if (x == 0){ is0 = true; Q.push(x); }
        else { isBelow0=!isBelow0; Q.push(-x); }
    }

    for (int i=0;i<k;++i){
        //printf("%lld\n",Q.top());
        long long d = Q.top();
        Q.pop();
        if (isBelow0){ Q.push(d+p); }
        else{ Q.push(abs(d-p)); if((d-p)<=0) isBelow0 = true; }
    }

    for (int i=0;i<n;++i) { /*printf("%lld\n",Q.top());*/ ans = (ans * (Q.top() % mod)) % mod; Q.pop(); } 
    if (isBelow0) ans = (mod - ans);
    printf("%d\n",ans);
    return 0;
}

【A T3 水管(Pipe)】
实质上就是求最小生成树是否唯一,因为求MST太简单了。
这里有两种做法,第一种是我的方法,我们考虑到,任意一条原图中的 (u,v) 加到最小生成树中都会形成一个环,那么若记这个环在最小生成树上的部分中边权最大的那条是 (u,v) ,那么 c(u,v)c(u,v) ,否则我们将这两条边互换,也就是删去 (u,v) 而将 (u,v) 加入最小生成树,那么边权和更小了,与假设相矛盾。而如果我们注意到当且仅当存在一对 c(u,v)=c(u,v) 的时候最小生成树不唯一,就可以马上导出我的做法了。我们先求一遍最小生成树,然后用树上倍增做出任意两个点的路径上的边权最大值,接着枚举不在最小生成树中的边,就可以直接判断了,时间复杂度 O(nlgn+nα(n))

# include <algorithm>
# include <iostream>
# include <cstring>
# include <cstdlib>
# include <cstdio>
# include <queue>
# include <ctime>
using namespace std;
const int maxn = 100010;
const int maxm = 200010;
const int maxj = 18;
struct Node{
    Node* next;
    int v,c;
    Node(){
        next = NULL;
    }
};
Node* G[maxn];
void insert(int u,int v,int c){
    Node* t = new Node();
    t->v = v;t->c = c; t->next = G[u]; 
    G[u] = t;
}

struct Edge{
    int u,v,c;
    bool operator <(const Edge& e) const{ return c < e.c; }
};
Edge e[maxm];
bool choose[maxm];

int fa[maxn];
int find_rt(int x){
    int t = x;
    while (fa[t]!=t) t = fa[t];
    int t1 = x,t2=fa[t1];
    while (fa[t1]!=t1){
        fa[t1] = t;
        t1 = t2;
        t2 = fa[t1];
    }
    return t;
}

int up[maxn][maxj];
int maxe[maxn][maxj];
int dep[maxn];
queue<int> Q;
bool vis[maxn];
void BFS(int ss){
    memset(vis,0,sizeof(vis));
    Q.push(ss); vis[ss] = true; dep[ss] = 0;
    for (int j=0;j<maxj;++j) up[ss][j] = -1;
    while (!Q.empty()){
    //  printf("%d\n",Q.front());
        for (Node* it=G[Q.front()];it!=NULL;it=it->next){
        //  printf("%d\n",it->v);
            if (!vis[it->v]){
            //  printf("(%d,%d)\n",Q.front()+1,it->v+1);
                vis[it->v] = true;
                dep[it->v] = dep[Q.front()]+1;
                up[it->v][0] = Q.front();
                maxe[it->v][0] = it->c;
                for (int j=1;j<maxj;++j) up[it->v][j] = -1;
                for (int j=1;j<maxj && up[up[it->v][j-1]][j-1] != -1;++j){
                    up[it->v][j] = up[up[it->v][j-1]][j-1];
                    maxe[it->v][j] = max(maxe[it->v][j-1],maxe[up[it->v][j-1]][j-1]);
                }
                Q.push(it->v);
            }
        }
        Q.pop();
    }
}

int find_max(int u,int v){
    int ans = 0;
    if (dep[u] > dep[v]) swap(u,v);
    for (int j=maxj-1;j>=0;--j){
        if (((dep[v]- dep[u]) & (1<<j)) != 0){ ans = max(ans,maxe[v][j]); v = up[v][j]; }
    }
    /*
    int ans1 = ans; //BruteForce
    int u1=u,v1=v;
    while (u1!=v1){
        ans1 = max(maxe[u1][0],ans1);
        ans1 = max(maxe[v1][0],ans1);
        u1 = up[u1][0];
        v1 = up[v1][0];
    }

    */
    while (u != v){
        for (int j=maxj-1;j>=0;--j){
            if (up[u][j] != up[v][j] || j == 0) {
                ans = max(ans,maxe[u][j]);
                ans = max(ans,maxe[v][j]);
                u = up[u][j];
                v = up[v][j];
            }
        }
    }

//  if (ans != ans1) printf("!!!!!!!!!!!!!\n");

    return ans;
}

int T,n,m;
int main(){
//  freopen("pipe.in","r",stdin);
//  freopen("pipe.out","w",stdout);
//  double tttt = clock();
    scanf("%d",&T);
    while (T--){
        scanf("%d%d",&n,&m);
        memset(G,0,sizeof(G));
        for (int i=0;i<m;++i){
            scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].c);
            --e[i].u; --e[i].v;
        }

        sort(e+0,e+m);

        long long ans = 0;
        for (int i=0;i<n;++i) fa[i] = i;
        for (int i=0;i<m;++i){
        //  printf("Now Deciding (%d,%d):%d\n",e[i].u+1,e[i].v+1,e[i].c);
            if (find_rt(e[i].u) != find_rt(e[i].v)){
            //  printf("Choose!\n");
                fa[find_rt(e[i].u)] = find_rt(e[i].v);
                insert(e[i].u,e[i].v,e[i].c);
                insert(e[i].v,e[i].u,e[i].c);
                ans += e[i].c;
                choose[i] = true;
            }
        }

        cout << ans << endl;

        BFS(0);
        /*
        for (int i=0;i<n;++i){
            for (int j=0;j<maxj;++j)
                printf("(%d,%d) ",(up[i][j] == -1 ? -1 : up[i][j]+1),maxe[i][j]);
            printf("\n");
        }
        */
        bool isOnly = true;
        for (int i=0;i<m;++i){
            if (!choose[i] && e[i].c == find_max(e[i].u,e[i].v)){
                isOnly = false;
                break;
            }
        }

        if (isOnly){ printf("Yes\n"); }
        else { printf("No\n"); }
    }

//  cout << clock()-tttt << endl;
    return 0;
}

这里有@Lanly的另一种做法,快了很多,也好写很多。我们考虑下我们求MST的时候,考虑到数据范围基本上都是用Kruskal,那么我们再次考虑下权值一样的那些边,显然如果存在一对满足条件的边在同一个环中,考虑下Kruskal的过程,对于后一条枚举的边(假设没选)我们可以判断一下和这条边同权值的已经扫过的那些边里有哪一条,是和这条边的两个端点在同一个并查集中的,那么容易看出这里就构成了个环。
不过直接枚举是显然超时的,我们可以随便搞个桶就好了,线性搞定。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值