2019 CCPC-Wannafly Winter Camp Day5 (Div2, onsite)(补题记录)

一篇来自ACM入门者姗姗来迟的补题记录…

A.Cactus Draw

题意:题目给定一棵树,要求把树在二维坐标平面上画出来,使得没有边相交。

思路:给每个结点构造合理的x,y坐标。

模仿样例把根结点坐标固定为(1,1),以结点的深度作为横坐标,从根结点开始进行一次DFS,结点的DFS序作为纵坐标,就能画出一个没有边相交的树。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1005;
vector<int> G[maxn];
int x[maxn],y[maxn];
int yt = 1;
void DFS(int fa,int me,int xt){
    x[me] = xt;
    y[me] = yt;
    int len = G[me].size();
    for(int i = 0;i<len;i++){
        int ne = G[me][i];
        if(ne == fa)
            continue;
        yt++;
        DFS(me,ne,xt+1);
    }
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 0;i<m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    DFS(-1,1,1);
    for(int i = 1;i<=n;i++)
        cout<<x[i]<<" "<<y[i]<<endl;
    return 0;
}

C.Division

题意:给定一个序列 a 1 ​ , a 2 ​ , … , a n a_1​,a_2​,…,a_n a1,a2,,an。每次选择其中一个数 a i a_i ai,使其变为 ⌊ a i ​ / 2 ⌋ ⌊a_i​/2⌋ ai/2,问对这些数进行k次操作以后,这些数总和的最小值为多少。

思路:签到题。

当k大于等于某个值的时候,答案是0,这个值就是 ( l o g ( a i ) + 1 ) (log(a_i)+1) (log(ai)+1)之和。
当k小于这个值的时候用优先队列模拟即可。
a i a_i ai的最大值为1e9,n的最大值为1e5,k不会超过3*1e6。

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    priority_queue<int> Q;
    long long sum = 0;
    int la = 0;
    for(int i = 0;i<n;i++){
        int a;
        scanf("%d",&a);
        sum += a;
        Q.push(a);
        la += (int)(log(a)/log(2)+1);
    }
    if(la <= k)
        printf("0");
    else{
        while(k--){
            int t = Q.top();
            if(t == 0)
                break;
            Q.pop();
            sum-=(t-t/2);
            Q.push(t/2);
        }
        printf("%lld",sum);
    }
    return 0;
}

F.Kropki

题意:有一个从1到n的排列 p 1 p_1 p1, p 2 p_2 p2 p n p_n pn,当 p i p_i pi p i + 1 p_{i+1} pi+1之间一个数是另一个数的两倍时,我们就会在这两个数之间画一个点,现在把数字全部擦去,以0/1串代表没点/有点,问满足这种排列条件的排列有多少种。

思路:状压dp。

d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]代表,集合状态为 k,以 j 结尾,长度为 i 的排列个数,状态 k 的二进制01串形式:第 i 位为0代表, i 这个数已经出现过了,1代表没有出现过。
所有状态可以用 k 表示为0~(1<<n)-1,k为0时代表没有数字出现过(空排列),k为(1<<n)-1时代表所有数字都出现了(全排列)。
那么我们的目标就是求: ∑ j = 1 n d p [ n ] [ j ] [ ( 1 &lt; &lt; n ) − 1 ] \sum_{j = 1}^{n} {dp[n][j][(1&lt;&lt;n)-1]} j=1ndp[n][j][(1<<n)1]

具体操作为先枚举长度i,再枚举结尾j,接着枚举由i变为i+1时的结尾k,当j不等于k且k、j满足题目给出的0/1串关系时,枚举状态p,选出含有j且不含有k的状态,将其转移到含有j也含有k的状态.
状态转移方程为: d p [ i + 1 ] [ k ] [ p ∣ ( 1 &lt; &lt; k ) ] + = d p [ i ] [ j ] [ p ] dp[i+1][k][p|(1&lt;&lt;k)]+=dp[i][j][p] dp[i+1][k][p(1<<k)]+=dp[i][j][p],注意取模。

小结:这是我的第一道状态压缩dp题,状压给我的感觉就是,将可以用01串形式也就是集合形式表达的状态,压缩成十进制,方便我们dp的for循环操作,提取状态时可以用位运算结合二进制的特点提取,算是入门了吧…

#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int pd(int a,int b){
    if(a*2 == b || b*2 == a)
        return 1;
    return 0;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    string s;
    cin>>s;
    int dp[n+1][n+1][1<<n] = {0};
    for(int i = 1;i<=n;i++)
        dp[1][i][1<<i-1] = 1;
    for(int i = 1;i<n;i++){   //枚举序列长度
        for(int j = 1;j<=n;j++)  //枚举结尾值
            for(int k = 1;k<=n;k++) //枚举下一个结尾值
                if(k != j && s[i-1] - '0' == pd(j,k))   //可行的方案
                    for(int p = 1;p<(1<<n);p++)
                        if((p& 1<< k - 1) == 0 && ( p& 1<< j - 1)!= 0)  //合理的状态
                            dp[i+1][k][p|(1<<k-1)] = (dp[i+1][k][p|(1<<k-1)]+dp[i][j][p])%mod;
    }
    long long ans = 0;
    for(int i = 1;i<=n;i++)
        ans = (ans + dp[n][i][(1<<n)-1]) %mod;
    cout<<ans<<endl;
    return 0;
}

H.Nested Tree

题意:含有 n n n个结点的树,将其复制 m m m遍,再用 m − 1 m-1 m1条边把所有树连起来,形成一棵大树,问这棵大树中所有点对的距离和。

思路:先将所有的树连起来,我们很难枚举所有点对再计算距离,我们考虑每条边的对答案的贡献。

一条边的长度为1,距离和其实是若干条路径之和,那么一条边的贡献就是所有路径经过这条边的次数,边 ( u , v ) (u,v) (u,v)的这个值等于 d [ u ] ∗ d [ v ] d[u]*d[v] d[u]d[v], d [ u d[u d[u]代表以 u u u为根结点的子树的结点个数, n &lt; 1 e 3 , m &lt; 1 e 3 n&lt;1e3,m&lt;1e3 n<1e3m<1e3,枚举边就好…

不过,直接枚举边好像也是一件不简单的事情…所以我们实际枚举的不是边,而是先假定一个根结点,以树形dp的形式枚举子结点,我们可以用回溯法处理出每个以该子结点u为根结点的子树的结点数d[u],然后这个结点u的贡献就是 d [ u ] ∗ ( n ∗ m − d [ u ] ) d[u]*(n*m-d[u]) d[u](nmd[u]),n*m就是总结点个数。

我们原本要枚举的边,其实就是结点u通往根结点的路径上的第一条边,子结点通往根结点的路径是唯一的,所以每条边与结点就是一一对应的(除了根结点没有边对应),这题让我深刻理解到了树的这个特点…

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
const int mod = 1e9+7;
vector<int>G[maxn];
long long ans = 0;
long long d[maxn] = {0};
int n,m;
void DFS(int fa,int x){
    d[x] = 1;
    for(int u:G[x]){
        if(u == fa)
            continue;
        DFS(x,u);
        d[x] += d[u];
    }
    ans = (ans+d[x]*(n*m-d[x]))%mod;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0;i<n-1;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        for(int j = 0;j<m;j++){
            G[u+n*j].push_back(v+n*j);
            G[v+n*j].push_back(u+n*j);
        }
    }
    for(int i = 0;i<m-1;i++){
        int a,b,u,v;
        scanf("%d%d%d%d",&a,&b,&u,&v);
        G[u+(a-1)*n].push_back(v+(b-1)*n);
        G[v+(b-1)*n].push_back(u+(a-1)*n);
    }
    DFS(0,1);
    cout<<ans<<endl;
    return 0;
}

I.Sorting

题意:你有一个数列 a 1 , a 2 , … , a n a_1, a_2, \dots, a_n a1,a2,,an你要模拟一个类似于快速排序的过程。有一个固定的数字 x x x。你要支持三种操作:
1. 1. 1.询问区间 [ l , r ] [l,r] [l,r]之间的元素的和,也就是 ∑ i = l r a i ​ \sum_{i=l}^r a_i​ i=lrai
2. 2. 2.对区间 [ l , r ] [l,r] [l,r]进行操作,也就是说你把区间中所有的数字拿出来,然后把小于等于 x x x的数字按顺序放在左边,把大于 x x x的数字按顺序放在右边,把这些数字接起来,放回到数列中。比如说 x = 3 x=3 x=3,你的区间里的数字是 1 , 5 , 3 , 2 , 4 1,5,3,2,4 1,5,3,2,4,那么操作完之后区间里面的数字变为 1 , 3 , 2 , 5 , 4 1,3,2,5,4 1,3,2,5,4
3. 3. 3.对区间 [ l , r ] [l,r] [l,r]进行操作,也就是说你把区间中所有的数字拿出来,然后把大于 x x x的数字按顺序放在左边,把小于等于 x x x的数字按顺序放在右边,把这些数字接起来,放回到数列中。

思路:巧妙的线段树题目。

注意到题目给出的排序方法,所有小于等于x的数的相对位置不会改变,所有大于x的数的相对位置不会改变。我们把原本序列中,小于等于 x x x的设置为0,大于 x x x的设置为1;

线段树维护区间内1的个数 s u m sum sum,那么0的个数自然为区间长度减去 s u m sum sum
p r e 0 [ ] , p r e 1 [ ] pre0[],pre1[] pre0[]pre1[]预处理所有小于等于 x x x的前缀和和所有大于 x x x的前缀和。

那么我们查询 [ l , r ] [l,r] [l,r]之和时,通过查询 [ 1 , l − 1 ] [1,l-1] [1,l1] s u m sum sum,和 [ 1 , r ] [1,r] [1,r] s u m sum sum,知道里面是第几个小于等于x的数,和第几个大于x的数,前缀和相减求答案。

排序即更新操作比较巧妙,先查询 [ l , r ] [l,r] [l,r]内1的个数 t o to to,再用线段树常规的 l a z y lazy lazy操作根据操作类型,更新相应长度的区间为0或1。

小结:这题得感谢一波队友,现场看到线段树直接暴力无限TLE。当天晚上和队友一起听题解,队友听懂了,我没懂(捂脸…),在队友讲解下补出来的…这应该算是我最早补的一道题了。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5;
int A[maxn];
long long pre0[maxn],pre1[maxn];//前方小于x的前缀和,前方大于x的前缀和
int n,q,x;
int tot0 = 0;
int tot1 = 0;
struct tree{
    int l,r,lazy,sum;
}E[maxn<<2];
void push_up(int rt){
    E[rt].sum = E[rt*2].sum+E[rt*2+1].sum;
}
void pushdown(int rt){
    if(E[rt].lazy != -1){
        E[rt*2].lazy = E[rt].lazy;
        E[rt*2+1].lazy = E[rt].lazy;
        E[rt*2].sum = (E[rt*2].r-E[rt*2].l+1)*E[rt].lazy;
        E[rt*2+1].sum = (E[rt*2+1].r-E[rt*2+1].l+1)*E[rt].lazy;
        E[rt].lazy = -1;
    }
}
void build(int l,int r,int rt){
    E[rt].l = l;
    E[rt].r = r;
    E[rt].lazy = -1;
    if(l == r){
        E[rt].sum = A[l] > x ? 1 : 0;
        return;
    }
    int mid = (r+l)/2;
    build(l,mid,rt*2);
    build(mid+1,r,rt*2+1);
    push_up(rt);
}
int query(int l,int r,int rt){
    if(l > r)
        return 0;
    if(E[rt].l == l && E[rt].r == r)
        return E[rt].sum;
    pushdown(rt);
    if(E[rt].l == E[rt].r)
        return E[rt].sum;
    int mid = (E[rt].l+E[rt].r)/2;
    if(mid >= r)
        return query(l,r,rt*2);
    else if(mid < l)
        return query(l,r,rt*2+1);
    else
        return query(l,mid,rt*2)+query(mid+1,r,rt*2+1);
}
void update(int l,int r,int rt,int flag){
    if(l > r)
        return;
    if(E[rt].l == l && E[rt].r == r){
        E[rt].sum = (E[rt].r-E[rt].l+1)*flag;
        E[rt].lazy = flag;
        return;
    }
    pushdown(rt);
    if(E[rt].r == E[rt].l){
        E[rt].sum = flag;
        return;
    }
    int mid = (E[rt].l+E[rt].r)/2;
    if(mid >= r)
        update(l,r,rt*2,flag);
    else if(mid < l)
        update(l,r,rt*2+1,flag);
    else{
        update(l,mid,rt*2,flag);
        update(mid+1,r,rt*2+1,flag);
    }
    push_up(rt);
}
int main()
{
    scanf("%d%d%d",&n,&q,&x);
    pre0[0] = pre1[0] = 0;
    for(int i = 1;i<=n;i++){
        scanf("%d",&A[i]);
        if(A[i] <= x)
            pre0[++tot0] = pre0[tot0-1]+A[i];
        else
            pre1[++tot1] = pre1[tot1-1]+A[i];
    }
    build(1,n,1);
    while(q--){
        int p,l,r;
        scanf("%d%d%d",&p,&l,&r);
        if(p == 1){
            int to1 = query(l,r,1);
            int to2 = query(1,l-1,1);
            long long ans1 = pre1[to1+to2]-pre1[to2];
            long long ans2 = pre0[r-to1-to2] - pre0[l-1-to2];
            printf("%lld\n",ans1+ans2);
        }
        else if(p == 2){
            int to = query(l,r,1);
            update(r-to+1,r,1,1);
            update(l,r-to,1,0);
        }
        else if(p == 3){
            int to = query(l,r,1);
            update(l+to,r,1,0);
            update(l,l+to-1,1,1);
        }
    }
    return 0;
}

J.Special Judge

题意:给出若干条边,问一共有多少条边相交,对于仅在端点相交的边不算相交。

思路:我感觉自己还是太年轻了,这个题面已经裸得不能再裸了,我手上也有线段相交板子,现场就是没有去开…

还是有一些小细节的,相交有三种方式:X形,T形,部分重叠。题目表示不存在重边和坐标相同的点,所以不会有完全重叠的边。

X形用叉积的方向性判断,后两种其实是一种——都有一个顶点位于另一条线段上,这样的点我一开始想用距离去判断,即当 d i s ( u , e ) + d i s ( u , s ) = d i s ( s , e ) dis(u,e)+dis(u,s)=dis(s,e) dis(u,e)+dis(u,s)=dis(s,e)时,点u在线段(s,e)上,但这样会WA掉,我估计是精度不够,只能用网上“叉积为0+坐标判断相对位置”的方法。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2005;
struct point{
    long long x,y;
}G[maxn];
int u[maxn],v[maxn];
double chacheng(point &a,point &b,point &c){     //计算叉积
    return (a.x-c.x)*(b.y-c.y)-(b.x-c.x)*(a.y-c.y);
}
bool relate(point &a,point &b){      //判点重合
    if(a.x == b.x && a.y == b.y)
        return true;
    return false;
}
bool ll(point &a,point &u,point &v){    //相对位置的判断
    if(chacheng(u,v,a) == 0 &&
       a.x >= min(u.x,v.x) && a.x <= max(u.x,v.x) &&
       a.y >= min(u.y,v.y) && a.y <= max(u.y,v.y))
        return true;
    return false;
}
bool pd(int s1,int s2,int e1,int e2){
    double a1 = chacheng(G[s1],G[e1],G[e2]);
    double a2 = chacheng(G[s2],G[e1],G[e2]);
    double b1 = chacheng(G[e1],G[s1],G[s2]);
    double b2 = chacheng(G[e2],G[s1],G[s2]);
    if(a1*a2 < 0 && b1*b2 < 0)
        return true;
    else if(a1*a2 > 0 || b1*b2 > 0)
        return false;
    else{
        if(ll(G[s1],G[e1],G[e2]) && !relate(G[s1],G[e1]) && !relate(G[s1],G[e2]))
            return true;
        if(ll(G[s2],G[e1],G[e2]) && !relate(G[s2],G[e1]) && !relate(G[s2],G[e2]))
            return true;
        if(ll(G[e1],G[s1],G[s2]) && !relate(G[e1],G[s1]) && !relate(G[e1],G[s2]))
            return true;
        if(ll(G[e2],G[s1],G[s2]) && !relate(G[e2],G[s1]) && !relate(G[e2],G[s2]))
            return true;
    }
    return false;
}
int main()
{
    std::ios::sync_with_stdio(false);
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 0;i<m;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        u[i] = a;
        v[i] = b;
    }
    for(int i = 1;i<=n;i++)
        scanf("%lld%lld",&G[i].x,&G[i].y);
    int ans = 0;
    for(int i = 0;i<m;i++)
        for(int j = i+1;j<m;j++)
            if(pd(u[i],v[i],u[j],v[j]))
                ans++;
    cout<<ans<<endl;
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值