HGOI20190710 题解

Problem A 游戏

有$n (1 leq n leq 5)$个硬币初始以"0"(正面),"1"(反面) $m (1 leq m leq m)$种操作,每一种操作可以将一些硬币进行翻转。

但是有$ k (0 leq k < frac{(m-1) imes m}{2}$) 组限制,每组限制描述$A$操作和$B$操作互不能连续进行。

已知翻转游戏进行$T$轮,求$T$轮之后,硬币全部翻转成反面朝上的方案数。

对于100%的数据 $1 leq T leq 10^5$

对于真正的100%的数据 $1 leq T leq 10^9 $

Sol : 对于100%的数据发现T的值比较小,暴力状压DP即可。

  设 $f_{i,j,k}$ 表示当前是第 $i$ 轮末,当前硬币的状态是 $j$ ,上一次转移而来的翻转操作的编号为 $k$。 

  转移方程为$f_{i,j,k} = sumlimits_{j'=0,k'=1 }^{j' leq 2^n-1 , k' leq m} f_{i-1,j',k'}$,

  其中限制是$j'$可以通过操作$k$变为$j$,并且操作$k$和操作$k'$可以连续进行。

  所以,这种算法的时间复杂度是$O(T imes 2^n m n )$,可以AC。

# pragma GCC optimize(3)
# include <bits/stdc++.h>
using namespace std;
const int mo=1e9+7;
int a[6],n,m,k,T,tmp[6];
vector<int>v[6];
int f[1000001][32][6];
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    int start=0;
    for (int i=1;i<=n;i++) {
        int t; scanf("%d",&t);
        start=(start<<1)+t;
    } 
    for (int i=1;i<=n;i++)
        for (int j=1;j<=n;j++) 
            v[i].push_back(j);
    for (int i=1;i<=m;i++) {
        int t; scanf("%d",&t);
         memset(tmp,0,sizeof(tmp));
        for (int j=1;j<=t;j++) { int o; scanf("%d",&o); tmp[o]=1;}
        int rr=0;
        for (int j=1;j<=n;j++) rr=(rr<<1)+tmp[j];
        a[i]=rr;
    }
    
    for (int i=1;i<=k;i++) {
        int x,y; scanf("%d%d",&x,&y);
        for (int j=0;j<v[x].size();j++)
            if (v[x][j]==y) v[x].erase(v[x].begin()+j);
        for (int j=0;j<v[y].size();j++)
            if (v[y][j]==x) v[y].erase(v[y].begin()+j); 
    }
    
    memset(f,0,sizeof(f));
    for (int i=1;i<=m;i++) {
        f[1][start xor a[i]][i]=1;
    }
    
    scanf("%d",&T);
    for (int i=2;i<=T;i++)
     for (int j=0;j<=(1<<n)-1;j++)
      for (int k=1;k<=m;k++)
        for (int t=0;t<v[k].size();t++) {
            int w=v[k][t];
            f[i][j][k]=(f[i-1][j^a[k]][w]+f[i][j][k])%mo;
        }
    int ans=0;
    for (int i=1;i<=m;i++) ans=(ans+f[T][(1<<n)-1][i])%mo;
    printf("%d
",ans);
    
    return 0;
}
Game_100.cpp

  而对于真正100%的数据则要使用矩阵加速DP,考虑到状态$(j',k')$可以变成$(j,k)$的条件是$j'$可以通过操作$k$变为$j$,并且操作$k$和操作$k'$可以连续进行。

  所以我们可以预处理出一个矩阵表示从某一个状态的二元组到另外一个状态的二元组的转移是否可行,如果可行置为1,不可行置为0.

  如何在普通的矩阵中表示一个转移的二元组呢?可以Hash一下,把二元组$ (j,k) $表示成$ j imes m + k$即可完成转移。

  实现方面,先预处理出T=1时的情况(不存在转移的合法性)作为初始矩阵,然后若转移$(j',k')$到$(j,k)$合法,那么就将$A[j'm+k'][jm+k]$置为1,否则置为0.

  然后对于矩阵A计算$A^{T-1}$表示,这个相同的转移方式一共进行T-1次,获得的矩阵再乘以初始矩阵就是最终的答案矩阵$Ans$了,

  最终的答案只需对$Ans$矩阵每个元素求和即可。

  复杂度是$O(log_2 T (2^n m + k)^3 )$足以通过$Tleq 10^9$的数据。

#include <fstream>
#include <algorithm>
using namespace std;

ifstream in("game.in");
ofstream out("game.out");

const int N = 200;
const long long P = 1000000007;
const int M = 6;

int operatorlist[M];
int n, m, t, s;
int start_bit = 0;
int ban[M][M];
long long tmp[N][N];
long long result[N][N];

void matrix_multiplication(int n, long long A[N][N], long long B[N][N])
{
    int C[N][N];
    for (int i=0; i<n; i++)
        for (int j=0; j<n; j++)
        {
            C[i][j] = 0;
        }
    for (int i=0; i<n; i++)
        for (int j=0; j<n; j++)
            for (int k=0; k<n; k++)
            {
                C[i][j] =(C[i][j]+(A[i][k]*B[k][j] % P))%P;
            }
    for (int i=0; i<n; i++)
        for (int j=0; j<n; j++)
        {
            A[i][j] = C[i][j];
        }
}

void matrix_addition(int n, long long A[N][N], long long B[N][N])
{
    for (int i=0; i<n; i++)
        for (int j=0; j<n; j++)
        {
            A[i][j]=(A[i][j]+B[i][j]) % P;
        }
}


void input()
{
    in>>n>>m>>t;
    for (int i=0; i<n; i++)
    {
        int x;
        in>>x;
        start_bit = (start_bit<<1)+x;
    }
    for (int i=0; i<m; i++)
    {
        int k, op = 0;
        in>>k;
        for (int j=0; j<k; j++)
        {
            int x;
            in>>x;
            op += 1<<(n-x);
        }
        operatorlist[i+1] = op;
    }
    for (int i=0; i<t; i++)
    {
        int x, y;
        in>>x>>y;
        ban[x][y] = 1;
        ban[y][x] = 1;
    }
    in>>s;
}
void work()
{
    for (int i=0; i<(1<<n); i++)
        for (int j=0; j<m; j++)
          for (int k=0; k<m; k++)
              if (ban[j+1][k+1]!=1)
              {
                  int next_i = i^operatorlist[k+1];
                  tmp[i*m+j][next_i*m+k] = 1;
              }
    int nn = (1<<n)*m;
    bool first = true;
    s-=1;
    while (s>0)
    {
        if (s&1)
        {
            if (first){ 
                matrix_addition(nn, result, tmp);
                first = false;
            }else
            {
                matrix_multiplication(nn, result, tmp);
            }

        }
        matrix_multiplication(nn, tmp, tmp);
        s>>=1;
    }
}

void output()
{
    long long ans =0;
    for (int i=0; i<m; i++)
        for (int j=0; j<m; j++)
        {
            ans = (ans + result[(start_bit^operatorlist[j+1])*m+j][((1<<n)-1)*m+i]) % P;
        }
    out<<ans<<endl;
    in.close();
    out.close();
}

int main()
{
    freopen("game.in", "r", stdin);
    freopen("game.out", "w", stdout);    
    input();
    work();
    output();
}
Game_100.cpp

 Problem B  探险

  给出一棵含有$n$个节点的树,和$m$条路径$s_i,t_i,d_i$完成下列两个任务。

  Rask1 : 对于每一条路径输出不在路径上的节点最小点的编号,(强制在线),

  Rask2:询问是否存在一个点$u$使得$u$到达路径$i$的距离(距离定义为点到路径的最短距离)都小于等于$d_i$ (要求输出"YES"和"NO")

  对于100%的数据$1 leq n,m leq 10^5$ 

Sol : 对于第一问,对每个节点都在其父亲的基础上建立可持久化线段树维护从根节点到每个节点路径上的编号在值域区间出现的总次数。

   对于一条路径$s_i , t_i ,d_i$求出$s_i,t_i$的LCA为$l_i$而$l_i$的直接父亲为$L_i$ ,

  由于每棵线段树的结构相同,所以在遍历线段树查询的时候,直接对$s_i$节点维护线段树+$t_i$节点维护的线段树-$L_i$-$l_i$节点维护的线段树的值域进行查找即可。

  这本质上用权值线段树对值域进行的二分查找。复杂度$O(n log_2 n)$

  对于第二问,贪心求解,对于每一条路径$s_i,t_i,d_i$求出$s_i,t_i$的LCA$l_i$,然后在$l_i$上跳$d_i$条边所得得节点设为$r_i$,如果不存在(跳到根节点以上)则不考虑。

  记深度最大的$r_i$节点为$R$,那么$R$是最优选取的点$u$,(其他可能的$u$可行的$R$一定可行,$R$若可行,其他可能节点$u$不一定行)。

  我们对上述贪心结论可以做出如下证明: (若 $R$节点对$s_i , t_i , d_i$不可行)

    1. 如果 $s_i , t_i $ 在$R$所在的子树中,那么其上跳$d_i$ 后若不可到达$R$ (由于其不可行) ,而与R的深度最大性不符合,不可能。

    2.如果 $s_i ,t_i $有$1$个节点在$R$所在子树中,那么路径必然经过节点$R$,所以其距离应该是$0$,而$d_i geq 0$是必然的,不符合,不可能。

    3.根据1,2两条结论我们可以发现,$s_i , t_i $必然不在$R$的子树中, 为了联系这个探险家,这个节点必然需要到$R$的子树外,所以在$R$的子树内的某一个探险家就无法被联系,不可能。

  综上所述,$R$必然是最优节点,所以我们只需要计算$R$与每条路径的距离即可。

  如果$R$被某一条路径经过,那么$R$到这条路径的距离是$0$ 否则就是$R$到$lca(s_i,t_i)$的距离。

 $Update : $怎么判断一个点$x$在树的路径$(u,v)$上呢?

 只要其$(dep_x geq dep_{lca}) And (LCA(u,x) = R) And (u$向上跳$dep_u - dep_x $步是$x)$ 或 $(dep_x geq dep_{lca}) And (LCA(v,x) = R) And (v$向上跳$dep_v - dep_x $步是$x)$ 即可  

  上述的两个问题总时间复杂度为$O(m log_2 n )$即可完成。

  Ps : 人傻自带大常数。

#pragma GCC optimize(3) 
# include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
const int B=25;
struct Path { int s,t,lca,d; }path[N];
struct Segment_Tree{ int ls,rs,cnt;}t[N*25];
struct rec{ int pre,to;}a[N<<1];
int root[N*25],g[N][30],head[N],dep[N];
int n,m,tot,size;
int R,valR;
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
inline void write(int x)
{
    if (x>9) write(x/10);
    putchar(x%10+'0');
}
void clear()
{
    tot=0; size=0; R=1; valR=0; root[0]=0;
    memset(head,0,sizeof(head));
    memset(t,0,sizeof(t));
    memset(a,0,sizeof(a));
}
void adde(int u,int v)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
#define lson t[rt].ls,l,Mid
#define rson t[rt].rs,Mid+1,r
#define Mid ((l+r)>>1)
void insert(int last,int &rt,int l,int r,int val)
{
    rt=++size; t[rt]=t[last];
    if (l==r) {t[rt].cnt++;return;}
    if (val<=Mid) insert(t[last].ls,lson,val);
        else insert(t[last].rs,rson,val);
    t[rt].cnt=t[t[rt].ls].cnt+t[t[rt].rs].cnt;
}
int query(int ru,int rv,int rlca,int rfa,int l,int r)
{
    if (l==r) return l;
    int ret=t[t[ru].ls].cnt+t[t[rv].ls].cnt-t[t[rlca].ls].cnt-t[t[rfa].ls].cnt;
    if (Mid-l+1>ret) return query(t[ru].ls,t[rv].ls,t[rlca].ls,t[rfa].ls,l,Mid);
    else return query(t[ru].rs,t[rv].rs,t[rlca].rs,t[rfa].rs,Mid+1,r);
}
void dfs(int u,int fath)
{
    g[u][0]=fath; dep[u]=dep[fath]+1;
    insert(root[fath],root[u],1,n,u);
    for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to; if (v==fath) continue;
        dfs(v,u);
    }
}
int LCA(int u,int v)
{
    if (dep[u]<dep[v]) swap(u,v);
    for (int i=26;i>=0;i--)
        if (dep[g[u][i]]>=dep[v]) u=g[u][i];
    if (u==v) return u;
    for (int i=26;i>=0;i--)
        if (g[u][i]!=g[v][i]) u=g[u][i],v=g[v][i];
    return g[u][0];
}
void init()    {
    for (int i=1;i<=26;i++)
        for (int j=1;j<=n;j++)
            g[j][i]=g[g[j][i-1]][i-1];
}
void jump(int u,int d)
{
    if (dep[u]-dep[1]<d) return;
    for (int i=26;i>=0;i--)
        if (d>=(1<<i)) d-=(1<<i),u=g[u][i];
    if (dep[u]>valR) valR=dep[u],R=u;    
}
int jumpto(int u,int d)
{
    if (d<0) return -1;
    for (int i=26;i>=0;i--)
        if (d>=(1<<i)) d-=(1<<i),u=g[u][i];
    return u;
}
int dis(int u,int v)
{
    int lca=LCA(u,v);
    return dep[u]+dep[v]-2*dep[lca];
}
int main()
{
    int T=read();
    while (T--) {
        clear();n=read();m=read(); 
        for (int i=1;i<n;i++) {
            int u=read(),v=read(); 
            adde(u,v); adde(v,u);
        }
        dfs(1,0); init();
        for (int i=1;i<=m;i++) {
            int s=read(),t=read(),d=read();
            int lca=LCA(s,t); jump(lca,d);            
            int ans=query(root[s],root[t],root[lca],root[g[lca][0]],1,n);
            write(ans);putchar('
');
            path[i].s=s; path[i].t=t; path[i].lca=lca; path[i].d=d;
        }
        bool check=true; 
        for (int i=1;i<=m;i++) {
            int u=path[i].s,v=path[i].t,lca=path[i].lca,d=path[i].d;
            if (dep[R]>=dep[lca]&&LCA(u,R)==R&&jumpto(u,dep[u]-dep[R])==R) continue;
            if (dep[R]>=dep[lca]&&LCA(v,R)==R&&jumpto(v,dep[v]-dep[R])==R) continue;
            if (dis(R,lca)>d) { check=false; break; }
        }
        if (check) putchar('Y'),putchar('E'),putchar('S'),putchar('
');
        else putchar('N'),putchar('O'),putchar('
');
    }
    return 0;
} 
explore.cpp

Problem C 或

T组询问,给出两个01串$a_i$和$b_i$,要求出一个$c_i$,

若在$a_i$和$b_i$中存在$a_x = 1$,$b_y = 1$,并且$x or y = i$,那么$c_i = 1$否则$c_i = 0$

对于100%的数据$1 leq |a| = |b| leq 2 imes 10^5$

Sol:  第一次接触二进制高维前缀和。

  我们对$i in [1,n]$分别考虑,如果$i$的二进制位表达$I$可以拆分成两个子集$A$和$B$并且使得$A cup  B = I$

  并且以二进制表达的A表示的十进制数作为下标$i$在$a_i$中不为0,并且以二进制表达B表示的十进制数作为下标的$j$在$b_j$中也不为0。

  那么$c_i = 1$否则$c_i = 0$ ,这是对原来题目的一步转化。

  为了判断当前位$i$是否合法,我们需要知道$s_i=sum_{Jsqsubseteq I} a_j$ 其中$I$表示$i$的二进制表示,$J$表示$j$的二进制表示。

  为了数学严谨上述公式可以被严谨的表示为$ s_i = sum _{j & i = i} a_j $

  我们如何对于上述定义下 某一数组$a_i$的对应的$s_i$数组,称为a的子集和。

  我们可以把a数组下标$i$当做二进制位拆分,一个“01”位表示一维,那么$s_i$就是$a_i$的$log_2 n $维前缀和。

  于是可以这么处理,先从低到高枚举维度$i$,然后枚举每一个下标$j$,如果该下标的第$j$位是$1$那么就是$i-1$维度$j$第$i$位是0或1两种情况的和。

  如果该第$j$位是$1$那么就是$i-1$维度$j$第$i$位是0一种情况的和。

  上面是基于一个很简单的DP,在实现过程中我们可能会记$f_{i,j}$表示第$i$维度第$j$位置的前缀和,但是我们很快就发现$i$的记录可以被滚动。

  于是就可以简单的用如下代码计算(每一次j循环开始的时候都是i-1维度时的前缀和,0维前缀和是原数列)

for i = 0 -> log_2 n+1 i++
    for j = 1 -> n j++
        if (j 第 i 位 是 1 ) a[j] = a[j] + a[去掉第 i 位的 1后的十进制下标];
        else a[j] = a[j] 

  我们回到原来的问题,对于$a_i$数组和$b_i$数组分别求出其子集和$s_1,s_2$,令$c_i = a_i imes b_i $那么$c$就是要求$c$方案总数的高维前缀和;

  然后对$c$做高维差分(就是高维前缀和逆运算)之后就是原来$c$的值,其含义是$c_i = 1$的取值$(x,y)$的组数。

  如果$c_i = 0 $输出0否则输出1即可。

  复杂度$O(T n log_2 n)$

# include<bits/stdc++.h>
# define int long long
using namespace std;
const int N=3e5+10;
char s[N];
int a[N],b[N],c[N];
signed main()
{
    int T; scanf("%d",&T);
    while (T--) {
        scanf("%s",s+1); int n=strlen(s+1);
        for (int i=1;i<=n;i++) a[i]=s[i]-'0';
        scanf("%s",s+1);
        for (int i=1;i<=n;i++) b[i]=s[i]-'0';
        
        int t=log(n)/log(2)+1;
        
        for (int i=0;i<=t;i++)
         for (int j=1;j<=n;j++)
          if (j&(1<<i)) a[j]+=a[j^(1<<i)];
        
        for (int i=0;i<=t;i++)
         for (int j=1;j<=n;j++)
          if (j&(1<<i)) b[j]+=b[j^(1<<i)];
          
        for (int i=1;i<=n;i++) c[i]=a[i]*b[i];
        
        
        for (int i=t;i>=0;i--)
         for (int j=1;j<=n;j++)
          if (j&(1<<i)) c[j]-=c[j^(1<<i)];
        
        for (int i=1;i<=n;i++) printf("%d",c[i]>0);
        puts("");   
    }

    return 0;
}
or.cpp
原文地址:https://www.cnblogs.com/ljc20020730/p/11164515.html