异或0等于自己
异或自己等于0
所以异或奇数次等于自己,偶数次等于0;
题意即可转换为所有数都用奇数次然后各个结果异或起来即为整个异或,一个操作改变一次奇偶性。等价于全部向下的coin每次对不同的k个coin翻转,使全部向上
这里忽略下标直接考虑两个无序队列,奇和偶,每一步翻转k个,从奇中翻转i个,偶中翻转k-i个得到新的状态,dp【i】为i个为奇时最小的步数(不一定连续),由此bfs搜索,并用path【】记录路径;路径记录的是传到此位置的原奇数个,算出last到now的差从两个队列中pop和push(顺便输出变动的序号)从一个pop出就要在另一个push,这两个队列是对立,分割的;
这里的关键是脱离了具体选取哪个,而抓住翻coin的本质划分为两个分割简化考虑
必要性不考虑,从性质的充分性出发,多半是对的,因为若不从性质入手直接考虑异或的运算,异或运算的逆运算的复杂多可能的,很难考率
明显性质有限。
#include<bits/stdc++.h>
using namespace std;
int path[505],vis[505],dp[505],n,k;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);//加速装置
cin>>n>>k;
memset(dp,0x3f,sizeof(dp));//图论初始化,有意义无穷大
dp[0]=0;//有0个为奇数次只用0次
queue<int>q;
q.push(0);
int flag=1;//题目限制提问次数
while(!q.empty()&&flag)
{
int s=q.front();
q.pop();
vis[s]=0;
for(int i=0;i<=k;++i)
{
if(i>s||(k-i)>(n-s))
continue;
int now=(s-i)+(k-i);
if(dp[now]>dp[s]+1)
{
dp[now]=dp[s]+1;
path[now]=s;//记录路径
if(vis[s]==0)
q.push(now),vis[now]=1;
}
if(dp[now]>500)
flag=0;
}
}
if(dp[n]>500)
flag=0;//只能赋值能找到的,防止找不到
if(flag==0)
{
cout<<-1<<endl;
}
else {
int res=0;
int num;
vector<int>a;
int s=n;
while(s!=k)
{
s=path[s];
a.push_back(s);
}
reverse(a.begin(),a.end());//逆转数组
a.push_back(n);
queue<int>q1,q2;//q1为偶数次的序号队列,q2为奇数次
for(int i=1;i<=n;++i)
{
q1.push(i);
}//初始化都在偶数次
for(int i=0;i<a.size();++i)
{
int t=a[i]-q2.size();//要多加几个奇数
int p=(k+t)/2;
cout<<"?";
for(int j=1;j<=p;++j)
{
int m=q1.front();
cout<<" "<<m;
q1.pop();
q2.push(m);
}//因为只与当前奇数偶数次有关,所以直接输出队列中元素无须选取,在奇偶次比较上都一致
for(int j=1;j<=k-p;++j)
{
int m=q2.front();
cout<<" "<<m;
q2.pop();
q1.push(m);
}
cout<<endl;
cout.flush();
cin>>num;
res^=num;//把所有的值异或
}
cout<<"! "<<res<<endl;
}
return 0;
}