cf 练习3

cf 955 div.2 D (二维前缀和 + 裴蜀定理)

设原本 有雪帽的点(设为1) 和 没有学帽(设为0)的点 差值为 dif

当边长为k的矩阵覆盖后 , 设矩阵中有x1个有雪帽的点和 x2个没有雪帽的点 ,那么此时的dif 值 就会减少为 (x2 -x1)*x ,假设x2 -x1 = m1  , 有n个子矩阵 ,所以题目就转化成了是否存在x1 ,x2 ,x3 ...xn 使得 x1*m1 + x2*m2 + ....+xn*mn =dif

这时,就需要用到裴蜀定理的推广 , 那么如果存在的话 , gcd(m1,m2..mn) | dif

对于如何求解m1 , m2 , m3..mn,就需要用到二维前缀和

 for(int i =1;i<=n;++i){
    for(int j =1;j<=m;++j){
      if(mp[i][j] == '1')sum1 +=a[i][j];
      else sum0 += a[i][j];
      pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + (mp[i][j] - '0');
    }
  }

我们预处理出了,每个子矩阵中1的数量 , 之后可以在建立一个数组存储出来每个m的值,m的值为k*k -val-val(val是一个子矩阵中1的数量 , k*k -val是子矩阵中0的数量)

 vector<int>num;
  //枚举所有的k阶子矩阵的数量

  for(int i =1;i+k-1<=n;++i){
    for(int j =1;j+k-1<=m;++j){
      int val = pre[i+k-1][j+k-1] - pre[i-1][j+k-1] - pre[i+k-1][j-1] + pre[i-1][j-1];//有val个1
      num.push_back(abs(k*k - val -val)); //这个才是差值
    }
  }

最后再使用裴蜀定理

int s = 0;
  for(const auto &t :num ){
    if(t)s = gcd(s , t);
  }
  if(s && dif%s == 0){
    cout<<"YES"<<endl;
  }
  else cout<<"NO"<<endl;

cf 959 div1 + div2  B. Fun Game

思路 : 先分类考虑一下 , 对于 第一次遇到 1 和 0 , 如何让1 变到 0 呢 ,可以直接让自己跟自己异或, 就变成 0 和 0 相同了 , 所以 1 0 这种情况可以轻松解决 。那么如果遇到了 0 和 1 , 该如何解决?我们就需要找到前面有 1 才可以 让0 和1 异或变成 1 , 所以我们只需要看前面是否有1,如果没有就输出 NO , 否则一定可以让s变成t

void solve(){
  cin>>n>>a>>b;
  bool f = false;
  bool ok = false;
  for(int i =0;i<n;++i){
    if(a[i] == '0')ok = true;
    if(a[i] == '1')f =true;
    
    if(a[i]  != b[i]){
     if(a[i] == '0' && (!f || !ok)){
      cout<<"NO"<<endl;
      return;
     }
    }
  }
  cout<<"YES"<<endl;
}

cf 959 div1 + div2 C  Hungry Games

Problem - C - Codeforces
思路 :我们从左端点l 开始 向后加al , al+1 , al+2... , 如果当我们的和超过了x,并且此时是r , 总和清零 ,前面的区间 对答案的贡献就是 r-1-l+1 = r-l , 清零后 ,又以r+1为新的左端点继续向后枚举,与刚刚的状态一样, 由此可以想到dp。dp[l] = dp[r+1] + r - l , 推出这样的状态后,可以发现状态时从后往前推的,所以我们可以倒着枚举.

int n , k;
int a[N] , f[N];
int pre[N];
/// fi =  fj + (j-1)-i+1 = fj + j-i
void solve(){
  cin>>n>>k;
  memset(f, 0 , sizeof(f)); //注意每次都要清空
  memset(pre , 0 , sizeof(pre));
  for(int i =1;i<=n;++i)cin>>a[i] , pre[i] = pre[i-1] + a[i];

  for(int i =n;i>=1;--i){
    int j = upper_bound(pre+1 , pre+1+n , k+pre[i-1])-pre; // 二分找到对于每个左端点到清零时的右端点 ,也就是上面说的 r
    if(j == n+1)f[i] = n - i+1; // 如果没有找到这个右端点 , 说明在这里的左端点一直到最后的n都是符合条件的
    else f[i] = f[j+1] + j-i; // 找到的话就状态转移
  }
  int ans = 0;
  for(int i =1;i<=n;++i)ans += f[i]; //最后把所有区间都加上
  cout<<ans<<endl;

}

cf 959 div1 + div2 D Funny Game

思路 : 如果x从1开始枚举的话 ,会有很多情况,所以我们可以考虑倒着枚举 , 最后倒着输出答案(我这里是采用栈来实现的).

x | (|au - av|) -> au \equiv av (mod x)
当 x = n-1 时 , ai %x 最多有 n -1种结果, 但是由鸽巢原理 ,因为总共有n个数 , 所以必定会有至少两个数字它们取模于x是相同的 , 所以这两个数字就可以连成一条边 ,之后我们可以删除其中一个数字(也就是删掉这个点 , 由样例知,我们尽可能删掉大的点)

当x = n-2时 , ai % x 最多有n-2种结果 , 还是由鸽巢原理 , (刚刚删掉了一个数),现在总共有n-1个数 , 所以必定会有至少两个数字,它们取模于x是相同的 , 那么这两个数字就连成一条边,之后再删除其中一个数字

以此类推 , 最后一定可以把所有点都连上

之后我们可以暴力枚举 , 复杂度是On^{2}
 

int n;
int a[N];
void solve(){
  cin>>n;
  for(int i =1;i<=n;++i)cin>>a[i];
  cout<<"YES"<<endl;
  map<int,int>mp;
  vector<pii>ans;
  for(int i =n-1;i>=1;--i){
    mp.clear(); // 每一次都要清零
    for(int j=1;j<=n;++j){
      if(a[j] == INF)continue;
      if(mp[a[j]%i]){
        ans.push_back({j , mp[a[j]%i]});
        a[j] = INF; //删点操作
        break;
      }
      mp[a[j]%i] = j;
    }
  }
  stack<pii>st;
  for(auto [x,y]:ans){
    st.push({x,y});
  }
  while(!st.empty()){
    cout<<st.top().first<<' '<<st.top().second<<endl;
    st.pop();
  }
}

cf 959 div1 + div2 E. Wooden Game

思路 : 这题纯诈骗题啊 , 题目给的树的数据都没有用, 真正有用的只有树的大小 , 因为我们可以随便删树的结点 , 所以我们只要暴力跑一遍删1- n(n表示树的大小),删哪个可以使得答案最大就行了

void solve(){
  cin>>n;
  int ans = 0;
  for(int i =1;i<=n;++i){
    int num;cin>>num;
    int c = ans;
    for(int j =1;j<=num;++j)ans = max(c|j , ans);
    for(int j = 1;j<num;++j){
      int x;cin>>x;
    }
  }
  cout<<max(ans , 1ll)<<endl;

}
  

cf 963 div2 C. Light Switches

思路 : 首先可以很轻易的看出每个房间都是有周期性的 ,整个周期为2k , 每个开灯或观灯周期为k。 那么对于最后一个房间,如果每个房间的加上一部分周期性(让他们进入最后一个房间开灯的时期),如果所有的房间都是在最后一个房间的开灯周期之内 ,那么就可以开灯 ,否则就输出-1

int n ,k;
int a[N];
void solve(){
  cin>>n>>k;
  int ma = 0;
  for(int i =1;i<=n;++i)cin>>a[i] , ma = max(ma , a[i]);

  for(int i =1;i<=n;++i){
    if((ma - a[i]) % (2*k) >= k){
      ma += 2*k - (ma - a[i])%(2*k) ;
    }
  }
    for(int i =1;i<=n;++i){
    if((ma - a[i]) % (2*k) >= k){
      cout<<-1<<endl;
      return;
    }
  }
  cout<<ma<<endl;
}

cf 963 div2 D Med-imize

思路 : 看到中位数就要想到二分 , 我们假设大于等于中位数的部分都是1 , 小于中位数的部分都是-1 , 那么如果总和大于等于 0 的话 , 说明二分的这个中位数成立。
所以我们就可以使用二分的方法 , 然后对于删数字 ,这题有一个非常经典的trick,连续在某一个位置删除三个数 ,以数组索引从0开始为例 , 对于0~n-1的数组 ,假设连续删除三个数字, 我们把索引全部取模于3  , 会得到若干的 0 1 2 0 1 2...这样的索引,连续删除三个数字 ,也就是删除0 1 2,三种索引,最后我们会留下 (n-1) % k +1 个数字。

那么怎么删除会使得剩下的数字中的中位数最大呢 ? 那么已可以使用dp , 因为他是对于每个(n-1)%k +1这样的一组数的状态转移

int n ,k;
int a[N] , f[N];
void solve(){
  cin>>n>>k;
  int ma = 0;
 for(int i =1;i<=n;++i)cin>>a[i];

  auto check = [&](int x){
    f[1] =a[1] >=x?1:-1; 
    for(int i =2;i<=n;++i){
      int v = a[i] >= x ? 1 : -1;
      if((i-1)%k == 0){
        f[i] = v;
      }
      else{
        f[i] = f[i-1] + v;
      }
      if(i > k)f[i] = max(f[i] , f[i-k]);
    }
    return f[n] > 0;

  };

 int l = 1 , r = 2e10;
 while(l < r){
  int mid = (l + r +1)>>1;
  if(check(mid))l = mid;
  else r = mid -1;
 }
 cout<<l<<endl;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值