Week2 题解
T2
首先,异或这个位运算有个很好的性质:x^y^y=x
于是,可以有两种解决方法:
法一
既然让猜原来的数是多少,当它异或了一个其他值val的时候,再异或val就会变回原来的值。
于是对于第一次输入的数y,让其对输出100个数异或,产生的结果中肯定有一个是原来的数;再对第二次的100个数重复上述操作,同时出现在两次结果中的数即是原来的x
但是会有一个问题:
第一次没用的结果中会出现:\(a_i\) ^\(a_j\) ^\(x\)
第二次没用的结果中会出现:\(a_l\) ^ \(a_r\) ^\(x\)
如果两个数恰好相等就会出现尴尬情形
于是,选数的时候就需要讲究点儿了——要保证第一组任两个数异或的值与第二组任两个数异或的结果不相同
这样的数的一种构造方法是:第一组让前七位都是0,则异或结果前七位都是1,而后七位由于数不相同不可能都是1;第二组让后七位都是0,则异或结果后七位都是1.
满足构造的一种情形是第一组输出1,2,3……100;第二组输出这些数左移7位的结果
法二
如果让第一组的前七位都是0,那么可以通过异或出的结果推知原来数前七位的情况;第二组后七位都是0,可以通过结果推知原来数后七位的情况,再拼接在一起即可
T3
CF1451
根据异或的性质,如果知道了第一个数的值,再异或剩余的数就可以恢复序列了,所以首先用第一个数异或剩下的N-1个数花费n-1次询问是固定的
easy version
嗯……比较富裕,可以花三次询问求出第一个数
先来介绍两个恒等式
\(a\&b+a\)^\(b=a|b\)
\(a\&b+a|b=a+b\)
于是,在知道两数与与两数异或后可以求出两数和,可以花三次询问前三个数两两与的值,从而列三元方程可以求出a的值
hard version
嗯……这回有点儿穷,得省着点儿用了
发现题中还有两个条件没有被用到——\(0 \le a_i\le n-1\);n为2的整数幂
那么可以分两种情况讨论:
-
若序列中两两数各不相同,体现为异或值各不相同且非零,那么异或的值中必然有一个数是1,意味着除最后一位都是与\(a_1\)是一样的;还有一个数必然是n-2,意味着除最后一位都是与\(a_1\)是不同的。与这两个数与再拼合即可
-
若序列中有重复的数,再分情况讨论:
-
其中有一个数与 \(a_1\)相同,即异或值为零,直接求即可
-
另两个数相等,即异或值相等,这样让它俩与求出值再求\(a_1\)的值即可
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5;
int n,b[maxn],vis[maxn],ans,a[maxn],x,y;
bool flag;
int main(){
cin>>n;
for(int i=2;i<=n;i++){
cout<<"XOR "<<1<<" "<<i<<endl;
fflush(stdout);
cin>>b[i];
if(b[i]==0&&(!flag)){
flag=true;
cout<<"AND "<<1<<" "<<i<<endl;
fflush(stdout);
cin>>a[1];
}
if(vis[b[i]]&&(!flag)){
flag=true;
cout<<"AND "<<vis[b[i]]<<" "<<i<<endl;
fflush(stdout);
cin>>ans;
a[1]=ans^b[i];
}
else vis[b[i]]=i;
}
if(!flag){
cout<<"AND "<<1<<" "<<vis[n-2]<<endl;
fflush(stdout);
cin>>x;
cout<<"AND "<<1<<" "<<vis[1]<<endl;
fflush(stdout);
cin>>y;
a[1]=x|y;
}
cout<<"! "<<a[1];
for(int i=2;i<=n;i++){
cout<<" "<<(b[i]^a[1]);
}
cout<<endl;
fflush(stdout);
return 0;
}
T4
这道题主要的难点就在于对角线的值为零,也就是说每次询问的时候尽量避免问到对线上的值,或者说如果对于一行i如果存在\(w_j=i\)那么这次询问对于这行是没有用的。
反过来的看,要让每一行的每一个数都有对这一行最小值贡献的机会。
由于每一个不在对角线上的位置的列号和行号不相同,即写成二进制位后至少有一位与行号的不同
那么可以枚举二进制的每一位,分别询问这一位是0和1的行们即可~
代码
#include<bits/stdc++.h>
using namespace std;
int n,cnt,ans[1005],x;
int main(){
cin>>n;
memset(ans,0x3f,sizeof ans);
for(int i=0;i<=9;i++){
cnt=0;
for(int j=1;j<=n;j++){
if(j&(1<<i))cnt++;
}
if(cnt){
cout<<cnt<<endl;
for(int j=1;j<=n;j++){
if(j&(1<<i))cout<<j<<" ";
}
cout<<endl;
fflush(stdout);
for(int j=1;j<=n;j++){
cin>>x;
if(j&(1<<i))continue;
ans[j]=min(ans[j],x);
}
}
cnt=0;
for(int j=1;j<=n;j++){
if(!(j&(1<<i)))cnt++;
}
if(cnt){
cout<<cnt<<endl;
for(int j=1;j<=n;j++){
if(!(j&(1<<i)))cout<<j<<" ";
}
cout<<endl;
fflush(stdout);
for(int j=1;j<=n;j++){
cin>>x;
if(!(j&(1<<i)))continue;
ans[j]=min(ans[j],x);
}
}
}
puts("-1");
for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
fflush(stdout);
return 0;
}
T5
首先,放置监听器数量的严格固定,相当于背包容量
这题有一个非常特殊的限制——“放置在节点 u 的监听设备并不监听 u 本身的通信”,于是对于一个点的状态并不只受当前点放不放的控制
另一个关键点是因为管辖半径为1,那么对于节点 u 来说,如果儿子没被监听,u也不监听,那么以后就没有机会了,所以对于 u 节点的子树,除 u 节点外其他节点必须已经被监听
于是设计状态: \(f[u][j][0/1][0/1]\)表示节点 u 的子树共使用 j 个监听器,u 节点是否放置, u 节点是否被监听,且此时其他点都被监听的方案数
由于涉及组合数学,可以考虑直接按照优化后的树形 dp 来写会更方便。
于是 4 种情况分别转移:
\(f[u][j+k][0][0]+=f[u][j][0][0]*f[v][k][0][1]\)
\(f[u][j+k][0][1]+=f[u][j][0][0]*f[v][k][1][1]+f[u][j][0][1]*(f[v][k][0][1]+f[v][k][1][1])\)
\(f[u][j+k][1][0]+=f[u][j][1][0]*(f[v][k][0][1]+f[v][k][0][0])\)
\(f[u][j+k][1][1]+=f[u][j][1][0]*(f[v][k][1][0]+f[v][k][1][1])+f[u][j][1][1]*(f[v][k][0][0]+f[v][k][0][1]+f[v][k][1][0]+f[v][k][1][1])\)
注意这道题 m 很小,而 \(size[u]\) 很大,需要取 min
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,maxm=2e5+5;
const int mod=1e9+7;
int f[maxn][105][2][2],g[maxn][105][2][2],hd[maxn],cnt,m,n,siz[maxn],x,y;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
struct Edge{
int nxt,to;
}edge[maxm];
void add(int u,int v){
edge[++cnt].nxt=hd[u];
edge[cnt].to=v;
hd[u]=cnt;
return ;
}
void dfs(int u,int father){
// cout<<u<<" ";
siz[u]=1;
f[u][0][0][0]=f[u][1][1][0]=1;
for(int i=hd[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==father)continue;
dfs(v,u);
for(int j=0;j<=min(m,siz[u]);j++){
g[u][j][0][0]=f[u][j][0][0],f[u][j][0][0]=0;
g[u][j][0][1]=f[u][j][0][1],f[u][j][0][1]=0;
g[u][j][1][0]=f[u][j][1][0],f[u][j][1][0]=0;
g[u][j][1][1]=f[u][j][1][1],f[u][j][1][1]=0;
// cout<<g[u][j][0][0]<<" "<<g[u][j][0][1]<<endl;
}
for(int j=0;j<=min(m,siz[u]);j++){
for(int k=0;k<=min(m-j,siz[v]);k++){
f[u][j+k][0][0]=(f[u][j+k][0][0]+1ll*g[u][j][0][0]*f[v][k][0][1]%mod)%mod;
f[u][j+k][1][0]=(f[u][j+k][1][0]+1ll*g[u][j][1][0]*((f[v][k][0][0]+f[v][k][0][1])%mod)%mod)%mod;
f[u][j+k][0][1]=(f[u][j+k][0][1]+(1ll*g[u][j][0][1]*((f[v][k][0][1]+f[v][k][1][1])%mod)%mod+1ll*g[u][j][0][0]*f[v][k][1][1]%mod)%mod)%mod;
f[u][j+k][1][1]=(f[u][j+k][1][1]+(1ll*g[u][j][1][0]*((f[v][k][1][0]+f[v][k][1][1])%mod)%mod+1ll*g[u][j][1][1]*((((f[v][k][0][0]+f[v][k][0][1])%mod+f[v][k][1][0])%mod+f[v][k][1][1])%mod)%mod)%mod)%mod;
}
}
siz[u]+=siz[v];
}
return ;
}
int main(){
n=read();
m=read();
for(int i=1;i<=n-1;i++){
x=read();
y=read();
add(x,y);
add(y,x);
}
dfs(1,0);
cout<<(f[1][m][1][1]+f[1][m][0][1])%mod;
return 0;
}
CF1479B2 Painting the Array II
这道题是个贪心
首先先去重是肯定的,接着就会遇到这样一个问题:
这样的情况下很明显5和3只能合并一个,贪心需要考虑这两个当中的抉择
最后的规则是这样的,哪个数下一次出现的位置近就优先合并哪个数
可以感性理解一下,远的和近的的贡献都是1,而合并了近的一个会为后面的数创造更多可能
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,a[maxn],f[maxn],ans,last[maxn],nxt[maxn],sta1[maxn],tp1,sta2[maxn],tp2;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
int main(){
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
last[i]=n+1;
}
n=unique(a+1,a+n+1)-a-1;
for(int i=n;i>=1;i--){
nxt[i]=last[a[i]];
last[a[i]]=i;
// cout<<nxt[i]<<" ";
}
// cout<<endl;
nxt[0]=n+1;
sta1[++tp1]=1;
ans++;
for(int i=2;i<=n;i++){
if(a[sta1[tp1]]==a[i])sta1[++tp1]=i;
else if(a[sta2[tp2]]==a[i]) sta2[++tp2]=i;
else{
if(nxt[sta1[tp1]]-i<nxt[sta2[tp2]]-i){
sta2[++tp2]=i;
}
else sta1[++tp1]=i;
ans++;
}
// for(int j=1;j<=tp1;j++)cout<<sta1[j]<<" ";
// cout<<endl;
// for(int j=1;j<=tp2;j++)cout<<sta2[j]<<" ";
// cout<<endl;
}
cout<<ans;
return 0;
}
CF1485D Multiples and Power Differences
构造神题
直接说做法吧,由于1~16所有可能值的乘积(这里比如2、4、8就可以被8代替)是720720 \(<\) 1000000
于是可以把对角线都填上720720,剩下的填 \(720720-x^4\)
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=505;
int n,m,a[maxn][maxn];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
if((i&1)==(j&1)){
cout<<720720<<" ";
}
else{
cout<<720720-pow(a[i][j],4)<<" ";
}
}
cout<<endl;
}
return 0;
}
又一道神奇的构造题
这道题里面难受的地方就在于加减的是 \(x*i\),自由性很小
这时看到了1这个数字,要是都由1来发出,那么加数就可以是任意的了
但是如果原来比平均数大的数怎么办?这些数是需要将多的还给1的,但是由于特殊的限制需要先从1给那些数填成倍数,再全还给1,再从1给过去,这样一来正好 \(3*(n-1)\) 次
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+5;
int t,n,a[maxn],tp,sum;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
struct Node{
int x,y,z;
Node(){}
Node(int a,int b,int c):x(a),y(b),z(c){}
}sta[maxn*3];
int main(){
t=read();
while(t--){
n=read();
sum=0;
for(int i=1;i<=n;i++){
a[i]=read();
sum+=a[i];
}
if(sum%n){
puts("-1");
continue;
}
tp=0;
for(int i=2;i<=n;i++){
a[1]+=a[i];
if(a[i]%i){
sta[++tp]=Node(1,i,i-a[i]%i);
a[i]+=i-a[i]%i;
}
sta[++tp]=Node(i,1,a[i]/i);
a[i]=0;
}
int ev=sum/n;
for(int i=2;i<=n;i++){
sta[++tp]=Node(1,i,ev);
}
cout<<tp<<endl;
for(int i=1;i<=tp;i++){
printf("%d %d %d\n",sta[i].x,sta[i].y,sta[i].z);
}
}
return 0;
}