2021年济南站icpc(2-SAT未补)

2021年济南站icpc

  • 导语
  • 涉及的知识点
  • 题目
    • C
    • E
    • K
    • M
  • 参考文献

导语

问就是后悔,说不定能拿银,至少能铜的,矩阵那个题,欲哭无泪,以后得算一下样例再看

涉及的知识点

搜索,组合数学,二维前缀和,dp,计算几何,2-SAT

链接:The 2021 ICPC Asia Jinan Regional Contest

题目

C

题目大意:有 n 件物品,第 i 件的价值为 a[i]。A 和 B 轮流取物品,A 先手。每个玩家都要最大化自己取到的物品的价值和,求有多少种可能的游戏过程。

思路:对于这道题目,需要从两个方面来分开考虑:在忽略下标的情况下,能够组成多少种合法的数字选取序列在获得了所有的合法序列的前提下,考虑下标,最后一共有多少种方案

对于第一个方面:因为需要得到最大和,那么每次去探讨当前的最大值即可,分成下面两种情况来考虑

  1. 最大值的个数为奇数
    那么当前回合的玩家必然会拿走一个最大值,剩下的就是偶数个最大值,另一个玩家可以有别的选择
  2. 最大值的个数为偶数
    如果当前玩家选了最大值,那么另一个玩家下一回合必然也会选择一个最大值

由这两种情况可以得出,任意偶数个数的数值在排列中必然是有两个连续的,举个例子,如 1 , 2 , 2 , 2 , 2 1,2,2,2,2 1,2,2,2,2这个序列最后分得的为 22122 22122 22122, 12222 12222 12222, 22221 22221 22221,由此可见是成对的,那么对于这些偶数个数值,求排列的问题就可以变成一对捆绑元素,插入n个空有多少种方式,显然可得 C n − k k C_{n-k}^k Cnkk种,对于奇数个数的数值,假设该数值为x,那么该x的位置固定在一对x之前,并且是第一个出现的x,举个例子,对于序列 1 , 2 , 2 , 2 , 2 , 2 1,2,2,2,2,2 1,2,2,2,2,2,最后分得 2 , 2 , 2 , 1 , 2 , 2 2,2,2,1,2,2 2,2,2,1,2,2 1 , 2 , 2 , 2 , 2 , 2 1,2,2,2,2,2 1,2,2,2,2,2 2 , 2 , 2 , 2 , 2 , 1 2,2,2,2,2,1 2,2,2,2,2,1,可以得知,当去掉一个x之后,确定了剩下的偶数对的位置,那么第一个去掉的x的位置是固定的,就在第一个偶数对之前,可得方案数 C n − k − 1 k C_{n-k-1}^k Cnk1k

那么,根据排列组合的规律,设当前数值出现的个数为 a i a_i ai,最后可以得到所有的忽略下标的排列有 ∏ i = 1 n C n − ∑ j = 1 i − 1 a i − ( a i + 1 ) / 2 a i / 2 \prod_{i=1}^nC_{n-\sum_{j=1}^{i-1}a_i-(a_i+1)/2}^{a_i/2} i=1nCnj=1i1ai(ai+1)/2ai/2

对于第二个方面:当我们获得了有多少种排列的时候,剩下的就好做了,对于每种排列中的每种数值,将该种数值视为一个整体,其相对位置排列有 a i ! a_i! ai!种,那么最后就可以得到公式: ∏ k = 1 n a i ! ∏ i = 1 n C n − ∑ j = 1 i − 1 a i − ( a i + 1 ) / 2 a i / 2 \prod_{k=1}^n a_i! \prod_{i=1}^nC_{n-\sum_{j=1}^{i-1}a_i-(a_i+1)/2}^{a_i/2} k=1nai!i=1nCnj=1i1ai(ai+1)/2ai/2

PS:感谢涛哥

代码

#include 
#define ll long long
#define mod 998244353
using namespace std;

ll jie[1000005];       //存储阶乘,在程序头预处理
ll num[1000005];       //存储每个数字出现的次数,在读入输入时处理

void exgcd(ll a,ll b,ll &x,ll &y);   //扩展欧几里得
ll inv(ll x);                        //逆元
ll C(ll m,ll n);  // which means select m from n
int main() {
    jie[0]=1;
    for(ll i=1; i<=1e6; i++) {
        jie[i]=jie[i-1]*i%mod;
    }
    ll n,n1;
    cin>>n;
    n1=n;
    for(int i=1; i<=n; i++) {
        ll b;
        cin>>b;
        num[b]++;
    }

    //定义了  cnt and ji 前者表示有多少种排列,后者表示对一个排列有多少种不同的选取先后顺序
    ll cnt=1;
    for(ll i=1e6; i>=1; i--) {
        if(num[i]==0) continue;
        //从大到小,注意,这是必须的。
        //简单来说,就是把两个相同的数字看成一个,然后就是在几个位置里选若干个,非常经典的排列组合
        ll k=num[i]/2;
        if(num[i]%2==1) {
            cnt=cnt*C(k,n-1-k)%mod;
        } else {
            cnt=cnt*C(k,n-k)%mod;
        }
        n-=num[i]; //在数字i结束后减去,递归的看更小的数字
    }
    ll ji=1; // 1 1 1 1 1 4 4 5 8 9 9 10              1 1 2 2 3 3
    for(ll i=1; i<=1e6; i++) {
        if(num[i]==0) continue;
        ji=ji*jie[num[i]]%mod;  //对一种给好的排列,每一种数字都有他的出现次数的阶乘的排列方法。这是基
    }
    cout<<cnt*ji%mod<<endl;
    return 0;
}

void exgcd(ll a,ll b,ll &x,ll &y) {
    if(b==0) {
        x=1;
        y=0;
        return;
    }
    exgcd(b,a%b,x,y);
    ll temp=x;
    x=y;
    y=temp-a/b*y;
}

ll inv(ll x) {
    ll x1,y1;
    exgcd(x,mod,x1,y1);
    x1=(x1%mod+mod)%mod;
    return x1;
}

ll C(ll m,ll n) {
    if(m>n) return 0;

    ll a=jie[n];
    ll b=inv(jie[m]);
    ll c=inv(jie[n-m]);
    return jie[n]*inv(jie[m])%mod*inv(jie[n-m])%mod;
}

E

题目大意:圆环上的n个点之间有m条连边,若两条边(a,b)(c,d)在圆内相交则产生(a+b)*(c+d)的权值。挑选两点删除与其相连的所有边令剩余权值最大化。

思路:去掉两点的权值和=总权值和-第一个点的贡献-第二个点的贡献+第一个点和第二个点的共同贡献
对于一条边 ( i , j ) (i,j) (i,j),它将圆分成左右两个圆弧,能与该边产生的交点的边 ( k , p ) (k,p) (k,p)满足 k ∈ ( i , j ) , p ∈ ( j , i ) k\in(i,j),p\in(j,i) k(i,j),p(j,i),因此可以推出对于边 ( i , j ) (i,j) (i,j)的贡献为 ( i + j ) × ∑ k = ( i + 1 ) % n ( j − 1 ) % n ∑ p = ( j + 1 ) % n ( i − 1 ) % n ( k + p ) (i+j)×\sum_{k=(i+1)\%n}^{(j-1)\%n}\sum_{p=(j+1)\%n}^{(i-1)\%n}(k+p) (i+j)×k=(i+1)%n(j1)%np=(j+1)%n(i1)%n(k+p)
累和的式子可以用二维前缀和求出,求出总权值和后枚举点对 ( i , j ) (i,j) (i,j),先计算 i i i的贡献,之后再计算删除i的前提下删除 j j j的贡献
i i i贡献的求解需要分两种情况

  1. i i i在当前边 ( x , y ) (x,y) (x,y)之内, x < y xx<y,通过二维前缀和计算 ( i , 1 ) (i,1) (i,1)的贡献减去 ( i , x − 1 ) (i,x-1) (i,x1)的贡献,最后得到 ( i , x ) (i,x) (i,x)区间内的贡献,如图
  2. i i i在当前边 ( x , y ) (x,y) (x,y)之外,那就在 ( y , x ) (y,x) (y,x)内,反向求解即可

2021年济南站icpc(2-SAT未补)_第1张图片

代码

#include 
#define int long long
using namespace std;
const int maxn=1e5+10;
int n,m,g[1212][1212],sum[1212],ans,rep[1212];
//g[x][y]表示圆上从x->y的圆弧上(包括x,y)的点与其余点连线的和
struct node {
    int x,y;
} e[maxn];
int cal(int x,int y,int xx,int yy) {
    if(x>xx||y>yy)return 0;
    return g[xx][yy]-g[x-1][yy]-g[xx][y-1]+g[x-1][y-1];
}
int ask(int x,int y) {
    if(cal(x,y,x,y)==0)return 0;
    if(x>y)swap(x,y);
    return cal(x+1,y+1,y-1,n)+cal(x+1,1,y-1,x-1);
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>n>>m;
    for(int i=1; i<=m; i++) {
        int x,y;
        cin >>x>>y;
        if(x>y)swap(x,y);
        e[i].x=x,e[i].y=y;//记录边
        g[x][y]+=x+y;//x->这一圆弧上的区间和加上了x+y
        g[y][x]+=x+y;
    }
    for(int i=1; i<=n; i++)
        for(int j=1; j<=n; j++)//获得所有区间的区间和
            g[i][j]+=g[i-1][j]+g[i][j-1]-g[i-1][j-1];
    for(int i=1; i<=m; i++) //获得所有的和
        ans+=(e[i].x+e[i].y)*ask(e[i].x,e[i].y);
    ans/=2;
    int acc=0;
    for(int i=1; i<=n; i++)//计算删去i点的贡献值
        for(int j=1; j<=m; j++) {//遍历每条边
            int t;
            if(e[j].x==i||e[j].y==i)continue;//遇到有i的点就去掉
            if(e[j].x+1==e[j].y)continue;//如果该边是相邻的两点直接跳过,因为不会有边相交
            if(i>e[j].x&&i<e[j].y)//计算对于当前边去掉i之后的贡献
                t=(e[j].x+e[j].y)*(cal(i,1,i,e[j].x-1)+cal(i,e[j].y+1,i,n));
            else
                t=(e[j].x+e[j].y)*cal(i,e[j].x+1,i,e[j].y-1);
            sum[i]+=t;
        }
    for(int i=1; i<=n; i++) {
        memset(rep,0,sizeof(rep));
        for(int j=1; j<=m; j++) {
            int t;
            if(e[j].x==i||e[j].y==i)continue;
            if(e[j].x+1==e[j].y)continue;
            if(i>e[j].x&&i<e[j].y)
                t=(e[j].x+e[j].y)*(cal(i,1,i,e[j].x-1)+cal(i,e[j].y+1,i,n));
            else
                t=(e[j].x+e[j].y)*cal(i,e[j].x+1,i,e[j].y-1);
            rep[e[j].x]+=t;//删除i时其余点的贡献
            rep[e[j].y]+=t;
        }
        for(int j=1; j<=n; j++) {//去掉j点,即去掉先前算其余点的贡献
            if(i==j)continue;
            acc=max(acc,ans-(sum[i]+sum[j]-rep[j]-(i+j)*ask(i,j)));
        }
    }
    cout <<acc;
    return 0;
}

K

题目大意:给出一棵n个节点的树,A在1号节点,B出现在剩余的n-1个节点的概率均相同,已知A走一条边会花费1s,求出A能找到B所花费时间的最小期望

思路:比赛的时候按照贪心的策略写,但其实没有必要,随便搜索即可

代码

#include 
using namespace std;
const int maxn=1e3;
int Size[maxn],t,fa[maxn],cnt,step,n;
int head[maxn],val[maxn];
bool vis[maxn];
typedef struct pp {
    int id;
    bool operator>(const pp a)const {
        return Size[id]<Size[a.id];
    }
} pp;
priority_queue<pp,vector<pp>,greater<pp>>gragh[101];
struct node {
    int next,to;
} e[maxn];
void Add(int from,int to) {
    e[++cnt].next=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}
void DFS1(int u,int f) {
    Size[u]=1;
    fa[u]=f;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v==f)continue;
        DFS1(v,u);
        Size[u]+=Size[v];
        gragh[u].push({v});
    }
}
void DFS2(int u,int f) {
    val[u]=step++;
    vis[u]=1;
    int len=gragh[u].size();
    for(int i=0; i<len; i++) {
        int v=gragh[u].top().id;
        gragh[u].pop();
        if(vis[v])continue;
        DFS2(v,u);
    }
    vis[u]=0;
    step++;
}
int main() {
    scanf("%d",&t);
    while(t--) {
        cnt=step=0;
        int ans=0;
        memset(head,0,sizeof(head));
        memset(val,0,sizeof(val));
        memset(vis,0,sizeof(vis));
        scanf("%d",&n);
        for(int i=1; i<=n; i++)
            while(gragh[i].size())gragh[i].pop();
        for(int i=0; i<n-1; i++) {
            int x,y;
            scanf("%d%d",&x,&y);
            Add(x,y);
            Add(y,x);
        }
        DFS1(1,1);
        DFS2(1,1);
        for(int i=2; i<=n; i++)ans+=val[i];
        printf("%.9f\n",(double)ans/(n-1));
    }
    return 0;
}

M

题目大意:在二维坐标系中有许多个与x轴平行放置的矩形,现在要给这些矩形涂色,涂色规则给出,规定了图案涂色的相对字典序,求能使得整个字典序最小的方案

思路:将矩形按照两条对角线划分成四个小三角形,对于任何一种涂黑的部分,必有两个三角形组成,将对面的两个小三角形分为一组,共两组,每组必有一个涂色,问题被转换为2n个变量的2-sat,若小三角形相交则不能同时选,划分的字典序可看做2n个变量并有相应的字典序,建立2-sat求字典序最小的解

代码

参考文献

  1. 【ICPC2021济南】E Insidemen

你可能感兴趣的:(ACM训练2021,贪心,思维,图论)