Loading

交互入门题瞎做

luogu P7045 「MCOI-03」金牌

题目链接

看到题解中介绍了一种用于找出序列中出现次数大于 \(\left\lfloor\dfrac{n}{2}\right\rfloor\) 的摩尔投票法。
先来贺一波题解给出摩尔投票法的具体操作:

  • 我们首先初始化变量 \(\text{ans=}a_1\) , \(\text{cnt=}1\)
  • 从此序列的第二个数开始扫描,直到第 \(n\) 个数 \(a_n\) ,我们假设现在扫描到了 \(a_i\)
  • 如果此时 \(\text{ans=}a_i\) 那么 \(\text{cnt}\leftarrow \text{cnt}+ 1\) 否则 \(\text{cnt}\leftarrow \text{cnt}- 1\)
  • 如果此时 \(\text{cnt=}0\) ,那么我们更新 \(\text{ans=}a_i\)
  • 当我们全部扫完之后,\(\text{ans}\) 就是出现次数大于 \(\left\lfloor\dfrac{n}{2}\right\rfloor\) 的数 。

我们现在着手来考虑这个东西和题目有什么联系。
经过简单的思考,我们可以发现当存在一个数出现次数大于 \(\left\lfloor\dfrac{n}{2}\right\rfloor\) 时,那么它就是无解的。
这个非常好理解,因为从鸽巢原理可以知道,此时一定会有两个相同的数它们是相邻的。

如果我们要求出每两个奖牌的磁性的关系是不可能的,所以我们可以参照摩尔投票法。

  • 我们维护一个队列 \(\text{que}\) ,满足:队列中所有的元素的磁性都是一样的(对标 \(\text{ans}\))。
  • 同时最开始的时候把 \(0\) 号奖牌放在答案序列(对标初始化)。
  • 然后大致的含义和上述相同:相同磁场压入队列,不同磁场压入答案数组。
  • 一直这样模拟最后不难发现会得到一个答案序列和一个队列 \(\text{que}\)

现在就是要解决那些多出来的出现次数最多的奖牌。
一种可以很好的放置多余的奖牌的方案就是在答案序列中插缝摆放,具体可以看代码。

点击查看代码
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>

#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
#define Enter putchar('\n')
#define quad putchar(' ')

const int N = 5e5 + 5;

int T, n, q, flag, ans[N], tot;
std::queue <int> que;

inline int ask(int id1, int id2) {
  printf("%d %d\n", id1, id2);
  fflush(stdout);
  int ret = 0; scanf("%d", &ret);
  return ret;
}

signed main(void) {
  std::cin >> T;
  for (int test = 1; test <= T; test ++) {
    scanf("%d %d", &n, &q);
    flag = -1; tot = 0; ans[++tot] = 0;
    while (!que.empty()) que.pop();
    for (int i = 1; i < n; i++) {
      if (que.empty()) {
        int tmp = ask(i, ans[tot]);
        if (tmp == 1) ans[++tot] = i;
        else que.emplace(i);
      } else {
        int tmp = ask(i, que.front());
        if (tmp == 1) {
          ans[++tot] = i, ans[++tot] = que.front();
          que.pop();
        } else que.emplace(i);
      }
    }
    for (int i = 2 * tot; i >= 1; i -= 2)
      ans[i] = ans[i / 2], ans[i - 1] = -1;
    if (que.size()) {
      bool last = 1;
      for (int i = 2; i <= 2 * tot; i += 2) {
        int tmp = ask(que.front(), ans[i]);
        if (tmp && last) ans[i - 1] = que.front(), que.pop();
        if (!que.size()) break;
        last = tmp;
      }
    }
    if (!que.empty()) {
      printf("-1\n");
      fflush(stdout);
    } else {
      printf("%d\n", n);
      for (int i = 1; i <= 2 * tot; i++)
        if (ans[i] != -1) printf("%d ", ans[i]);
      printf("\n");
      fflush(stdout);
    }
  }
  return 0;
}

CF1033E Hidden Bipartite Graph

题目链接

这题看上去一脸不可做,对,我看什么题都不可做。。。
然后瞄一眼题解,发现一个小 \(\tt Trick\)
判定二分图可以先拉出一个生成树,対生成树进行染色然后看相同颜色内有没有连边。

所以现在的第一步是拉出一个生成树。
首先,我们先把题目中要求的交互函数写出来,我用一个 \(\tt vector\) 记录查询的点集。
同时在我自己测试时发现可能会询问重复的点集,所以用一个 \(\tt map\) 来记录已经查过的答案。

inline int ask(std::vector<int> chose) {
  if (chose.size() == 1) return 0;
  std::sort(chose.begin(), chose.end());
  if (ma[chose]) return ma[chose];
  printf("? ");
  write(chose.size()), Enter;
  for (int t : chose) write(t), quad; Enter;
  fflush(stdout);
  int ret; read(ret); 
  ma[chose] = ret; return ret;
}

接下来就按照生成树的角度进行思考。
首先我们需要并查集,这个非常简单不在累述,然后我们会发现要进行 \(n-1\) 次连边操作。
对于每个连边操作,我们都要找到一个和根节点所在集合有边的点 \(p\) 然后连边。
那么怎么找到这样的点 \(p\) 呢?这里有一个显然的结论:

对于点集 \(A\)\(B\) ,如果 \(A\)\(B\) 中的点有边相连,那么满足 \(ask(A)+ask(B)<ask(A\cup B)\)

运用这个结论,我们就可以找到上文所讲的 \(p\)

我们令根节点所在的点集为 \(A\) ,其他的点构成的点集为 \(B\)
同时我们令上文结论中的查询方式为 \(check(A,B)\) ,及调用 \(check(A,B)\) 就可以知道是否有边。
因为询问次数控制较为严格,我们考虑 \(O(n\log n)\) 的较大常数做法。
直接能够想到的是二分做法:(假设 \(B\) 集合的大小为 \(L\)

  • 我们把 \(B\) 按照大小平均分成两个集合 \(B_1\)\(B_2\)
  • 分别查询 \(check(B_1,A)\)\(check(B_2,A)\) ,如果一个为真则取为真的,否则任意取一个。
  • 不难发现,最后集合 \(B\) 只会剩下一个节点,那个节点就是 \(p\) 。复杂度 \(O(\log n)\)

找到了 \(p\) ,我们还要知道 \(p\) 和根节点集合中的哪一个点有边,按照相似的方法即可。
只不过此次查询的 \(check\) 操作更为简洁,复杂度还是 \(O(\log n)\)

重复 \(n-1\) 次上述的操作,我们就找到了一个生成树,接下来对树染色非常简单。
我们令染为白色和黑色的点集分别为 \(white\)\(black\) ,进行一次 \(check(white,black)\) 即可判断二分图。
如果是二分图,那么接下来非常简单,现在来讨论非二分图的情况。

我的做法是随机化,每一次对集合进行一次 \(\tt random_shuffle\) ,然后取 \(\frac{L}{2}\)
进行查询,如果可以的话让点集大小直接减半,不知道对不对,反正我过了

所以这样下来,复杂度约为 \(O(n\log n)\) 带上 \(3\sim 5\) 倍常数,可以通过。
具体可以看代码:

点击查看代码
#include <set>
#include <map>
#include <cmath>
#include <queue>
#include <string>
#include <cstdio>
#include <cctype>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_map>

#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
#define quad putchar(' ')
#define Enter putchar('\n')

using std::abs;
using std::pair;
using std::string;
using std::make_pair;

// #define int long long

template <class T> void read(T &a);
template <class T> void write(T x);

template <class T, class ...rest> void read(T &a, rest &...x);
template <class T, class ...rest> void write(T x, rest ...a);

const int N = 1005;

int n, root_edge, tot, ok[N], edgetot, col[N];
int deep[N], fa[N][15], sta[N], top;
std::vector <int> now, rt, white, black;
std::vector <int> dis[N];

std::map <std::vector<int>, int> ma;

struct Edge {
  int x, y;
  Edge (int _x = 0, int _y = 0) {x = _x; y = _y;}
} edge[N * N];
namespace UFST {
int fa[N], siz[N];
inline int find(int);
inline void rebuild(int);
inline void merge(int, int);
}

inline int ask(std::vector<int> chose) {
  if (chose.size() == 1) return 0;
  std::sort(chose.begin(), chose.end());
  if (ma[chose]) return ma[chose];
  printf("? ");
  write(chose.size()), Enter;
  for (int t : chose) write(t), quad; Enter;
  fflush(stdout);
  int ret; read(ret); 
  ma[chose] = ret; return ret;
}
inline bool check(int l, int r) {
  now.clear();
  for (int i = l; i <= r; i++) now.emplace_back(ok[i]);
  int edge1 = ask(now);
  for (int t : rt) now.emplace_back(t);
  int edge2 = ask(now);
  if (edge1 + root_edge < edge2) return true;
  return false;
}
inline bool check2(int l, int r, int p) {
  now.clear();
  for (int i = l; i <= r; i++) now.emplace_back(ok[i]);
  int edge1 = ask(now);
  now.emplace_back(p);
  int edge2 = ask(now);
  if (edge1 < edge2) return true;
  return false;
}

inline void DFS(int, int);
inline int LCA(int, int);

signed main(void) {
  read(n); UFST::rebuild(n);
  if (n == 1) {
    printf("Y 1 \n1");
    return 0;
  }
  now.emplace_back(1); 
  root_edge = ask(now);
  rt = now;
  for (int edgenum = 1, rootteam; edgenum < n; edgenum++) {
    root_edge = ask(rt);
    tot = 0, rootteam = UFST::find(1);
    for (int i = 1; i <= n; i++)
      if (UFST::find(i) != rootteam) ok[++tot] = i;
    int left = 1, right = tot, mid;
    while (left < right) {
      mid = (left + right) / 2;
      if (check(left, mid)) right = mid;
      else left = mid + 1; 
    }
    int point = ok[left];
    tot = 0;
    for (int t : rt) ok[++tot] = t;
    left = 1; right = tot;
    while (left < right) {
      mid = (left + right) / 2;
      if (check2(left, mid, point)) right = mid;
      else left = mid + 1;
    }
    UFST::merge(ok[left], point);
    edge[++edgetot] = Edge(ok[left], point);
    rt.emplace_back(point);
  }
  for (int i = 1; i <= edgetot; i++) {
    Edge p = edge[i];
    dis[p.x].emplace_back(p.y);
    dis[p.y].emplace_back(p.x);
  }
  DFS(1, 0);
  for (int i = 1; i <= n; i++) {
    if (col[i] == 0) white.emplace_back(i);
    else black.emplace_back(i);
  }
  // for (int num : white) write(num), quad; Enter;
  // for (int num : black) write(num), quad; Enter;
  int edge1 = ask(white), edge2 = ask(black);
  // printf("!!!");write(white.size(), edge1);
  int p1, p2;
  if (edge1 == 0 && edge2 == 0) {
    printf("Y "); write(white.size()), Enter;
    for (int num : white) write(num), quad; Enter;
    return 0;
  } else if (edge1 != 0) {
    tot = 0;
    for (int num : white) ok[++tot] = num;
    while (1) {
      now.clear();
      std::random_shuffle(ok + 1, ok + 1 + tot);
      for (int i = 1; i * 2 - 1 <= std::max(tot, 3); i++) now.emplace_back(ok[i]);
      // for (int t : now) write(t), quad; Enter; write(ask(now)); Enter;
      if (ask(now)) { 
        if (now.size() == 2) {p1 = ok[1]; p2 = ok[2]; break;}
        tot = (tot + 1) / 2;
      }
    }
  } else if (edge2 != 0) {
    tot = 0;
    for (int num : black) ok[++tot] = num;
    while (1) {
      now.clear();
      std::random_shuffle(ok + 1, ok + 1 + tot);
      for (int i = 1; i * 2 - 1 <= tot; i++) now.emplace_back(ok[i]);
      if (ask(now)) { 
        if (now.size() == 2) {p1 = ok[1]; p2 = ok[2]; break;}
        tot = (tot + 1) / 2;
      }
    }
  }
  printf("N "); 
  int lca = LCA(p1, p2);
  write(deep[p1] + deep[p2] - 2 * deep[lca] + 1); Enter;
  while (1) {
    write(p1), quad;
    p1 = fa[p1][0];
    if (p1 == lca) break;
  } 
  while (1) {
    sta[++top] = p2;
    if (p2 == lca) break;
    p2 = fa[p2][0];
  }
  while (top) write(sta[top]), quad, top --; Enter;
  return 0;
}

inline void DFS(int now, int father) {
  deep[now] = deep[father] + 1;
  col[now] = 1 - col[father];
  for (int i = 0; i <= 12; i++) fa[now][i + 1] = fa[fa[now][i]][i];
  for (int t : dis[now]) {
    if (t == father) continue;
    fa[t][0] = now;
    DFS(t, now);
  }
}
inline int LCA(int x, int y) {
  if (deep[x] < deep[y]) std::swap(x, y);
  for (int i = 13; i >= 0; i--)
    if (deep[fa[x][i]] >= deep[y]) x = fa[x][i];
  if (x == y) return x;
  for (int i = 13; i >= 0; i--)
    if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
  return fa[x][0];
}

namespace UFST {
inline int find(int x) {
  return x == fa[x] ? x : fa[x] = find(fa[x]);
}
inline void rebuild(int n) {
  for (int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
}
inline void merge(int x, int y) {
  x = find(x); y = find(y);
  if (x == y) return ;
  if (siz[x] > siz[y]) std::swap(x, y);
  fa[x] = y; siz[y] += siz[x];
}
}

template <class T> void read(T &a) {
  int s = 0, t = 1;
  char c = getchar();
  while (!isdigit(c) && c != '-') c = getchar();
  if (c == '-') c = getchar(), t = -1;
  while (isdigit(c)) s = s * 10 + c - '0', c = getchar();
  a = s * t;
}
template <class T> void write(T x) {
  if (x == 0) putchar('0');
  if (x < 0) putchar('-'), x = -x;
  int top = 0, sta[50] = {0};
  while (x) sta[++top] = x % 10, x /= 10;
  while (top) putchar(sta[top] + '0'), top --;
  return ;
}

template <class T, class ...rest> void read(T &a, rest &...x) {
  read(a); read(x...);
}
template <class T, class ...rest> void write(T x, rest ...a) {
  write(x); quad; write(a...);
}

CF1129E Legendary Tree

题目链接

一道自认为很有意思的交互题。
题目给出的询问方式看上去非常申必,但是我们可以从树的性质下手进行分析。

我们假定这个树的根节点是 \(1\) ,那么很显然,我们可以进行 \(n-1\) 次询问。
对于第 \(i\) 次询问,我们是这样的:

\[\{\{1\},\{2,3,\cdots ,n-1,n\},i\} \]

这个表示从根节点出发,经过 \(i\) 节点最后能到达树上的几个节点。
几乎不用想的,上述询问直接给出了第 \(i\) 个节点的子树大小 \(siz\)
现在每一个节点的子树大小都确定了,问题转化成为对于节点 \(i\) 确定它的父亲节点是什么。

考虑到随着节点深度的不断增加,节点的子树大小一定不断减小。
所以我们按照 \(siz\) 从小到大排序,这样的话对于每一个节点 \(i\) ,它的儿子一定是在它的左边。
现在问题就是在 \(i\) 左边所有没有被选择的节点中高效率地找到 \(i\) 的儿子们。

在这里,我们可以用二分的方法来解决这个问题:
我们假定现在已经扫到了第 \(i\) 个节点,我们记 \(i\) 左边没有被选的节点集合为 \(S\)
首先,二分的左边界 \(L\) 一定是 \(1\) ,有边界我们定为 \(|S|\) ,及 \(S\) 集合的大小。
我们记此时二分出的值为 \(mid\) ,那么我们进行如下的询问:

\[\{\{1\},\{S_1,S_2,\cdots ,S_{mid-1},S_{mid}\}, u\} \]

其中 \(u\) 表示当前扫到的节点的编号。
当我们发现询问的值大于 \(0\) 时,我们就缩小范围,否则就增大范围。
最后我们要找的是满足上述询问大于 \(0\) 的最小的 \(pos\)\(pos\) 表示排完序后的节点编号。
容易发现,其实最后一个 \(pos\) 位置上的节点一定是 \(u\) 的孩子,\(u\) 的定义如上。

这样进行不断的询问,可以发现询问次数是 \(O(n^2\log n)\) 级别的,显然可以通过。

点击查看代码
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>

#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)

const int N = 505;

int n, root, siz[N], fa[N], st[N], tot;
struct Node {
  int id, siz;
  Node (int _id = 0, int _siz = 0) {
    id = _id; siz = _siz;
  }
  friend bool operator<(const Node &p, const Node &q) {
    return p.siz < q.siz;
  }
} node[N];

inline int ask_size(int point) {
  printf("1\n1\n");
  printf("%d\n", n - 1);
  for (int i = 2; i <= n; i++) printf("%d ", i);
  printf("\n%d\n", point);
  fflush(stdout);
  int ret; scanf("%d", &ret);
  return ret;
}

inline int ask(int point, int right) {
  printf("1\n1\n");
  printf("%d\n", right);
  for (int i = 1; i <= right; i++) printf("%d ", st[i]);
  printf("\n");
  printf("%d\n", point);
  fflush(stdout);
  int ret; scanf("%d", &ret);
  return ret;
}

signed main(void) {
  scanf("%d", &n);
  root = 1; siz[root] = n;
  for (int i = 2; i <= n; i++) siz[i] = ask_size(i);
  for (int i = 1; i <= n; i++) node[i] = Node(i, siz[i]);
  std::sort(node + 1, node + 1 + n);
  for (int i = 1; i <= n; i++) {
    while (1) {
      tot = 0;
      for (int j = 1; j < i; j++) 
        if (fa[node[j].id] == 0) st[++tot] = node[j].id;
      int left = 1, right = tot, ans = n + 1;
      while (left <= right) {
        int mid = (left + right) / 2;
        if (ask(node[i].id, mid)) {
          right = mid - 1;
          ans = std::min(ans, mid);
        } else left = mid + 1;
      }
      if (ans == n + 1) break;
      fa[st[ans]] = node[i].id;
    }
  }
  printf("ANSWER\n");
  for (int i = 2; i <= n; i++)
    printf("%d %d\n", fa[i], i);
  return 0;
}

CF1705F Mark and the Online Exam

题目链接

看上去像是经典交互问题,但是好像不太会……

这道题的一种非常直接的想法也是第一步就是先全部试一下 \(\text{T}\) 然后你就能得到一共有多少个答案为 \(\text{T}\)

然后最暴力的方法就是每一次把其中一个选项变成 \(\text{F}\) 然后查询结果。
正确性是没得讲,但是这样操作的询问次数是 \(O(n)\) 的级别,会 \(\text{Wrong Answer on test 9}\)

下面是一种看到的非常巧妙的构造询问的方法:
看到 \(n\) 和询问次数的数量级关系,容易判断出询问的次数要是 \(\frac{n}{3}\) 级别才不会有问题。
我们考虑把这 \(n\) 个选择题平均地分成三个部分,同时为了方便,我们定义 \(m=\left\lfloor\dfrac{n}{3}\right\rfloor\)

令序列 \(S\) 为询问时我们的答案,\(\text{ret=ask(S)}\) 表示一次询问操作。

  1. 最简单的一步令 \(S=\{T,T,\cdots, T\}\)\(\text{num=ask(S)}\) 表示答案中有多少个 \(\text{T}\)
  2. 对于每一个 \(i\ (i\leq m)\) ,我们把 \(i\)\(i+m\) 这两位变成 \(\text{F}\) 其他的位仍然是 \(\text{T}\) ,进行询问 \(\text{ret=ask(S)}\) ,然后对 \(\text{ret}\) 进行分讨。
    • \(ret=num+2\) 说明这两个猜对了,所以 \(i\)\(i + m\) 这两位都是 \(\text{F}\)
    • \(ret=num-2\) 说明两个都猜错了,所以 \(i\)\(i + m\) 这两位都是 \(\text{T}\)
    • \(ret=num\) 说明一个猜对一个猜错了,我们先放一边,不进行考虑。
  3. \(S'=m\times \text{F} + (n-m)\times \text{T}\) ,进行询问 \(\text{q3=ask(S')}\)
  4. 对于每一个 \(i\ (i\leq m \text{且第} \ i\ \text{位还没有被确定})\),我们把 \(S'\) 的第 \(i\) 变成 \(\text{T}\),第 \(i+m\)\(i+2m\) 位变成 \(\text{T}\) ,在对其进行询问 \(\text{ret=ask(S'')}\)
    • \(ret = q3+3\) 三个都猜对了,答案就是 \(\text{TFF}\)
    • \(ret = q3-3\) 三个都猜错了,答案是 \(\text{FTT}\)
    • \(ret = q3+1\) 对了两个错了一个,结合上述的分析以及 情况2 的排查可以得出答案是 \(\text{TFT}\) ,在这里不再证明。
    • \(ret = q3-1\) 对了一个错了两个,同理,答案是 \(\text{FTF}\)
  5. 对于所有还没有确定的,直接按照最暴力的方法去做就可以了。

容易发现询问次数是 \(O(\left\lfloor\dfrac{n}{3}\right\rfloor)\) 级别的,没有任何问题。

点击查看代码
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <random>

#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
#define Enter putchar('\n')

const int N = 1e3 + 5;
int n, ans[N], visit[N], num, out[N], ret, q3;

inline int ask(int *ans) {
  for (int i = 1; i <= n; i++) {
    if (ans[i] == 1) printf("T");
    else printf("F");
  } Enter;
  fflush(stdout);
  int ret; scanf("%d", &ret);
  if (ret == n) exit(0);
  return ret;
}

signed main(void) {
  scanf("%d", &n); 
  memset(out, -1, sizeof(out));
  for (int i = 1; i <= n; i++) ans[i] = 1;
  num = ask(ans);
  int m = n / 3;
  for (int i = 1; i <= m; i++) {
    int x = i, y = i + m;
    ans[x] = ans[y] = 0;
    ret = ask(ans);
    if (ret == num + 2) out[x] = out[y] = 0;
    else if (ret == num - 2) out[x] = out[y] = 1; 
    ans[x] = ans[y] = 1; 
  }
  std::fill(ans + 1, ans + 1 + n, 1);
  for (int i = 1; i <= m; i++) ans[i] = 0;
  q3 = ask(ans);
  for (int i = 1; i <= m; i++) {
    if (out[i] != -1) continue;
    ans[i] = 1, ans[i + m] = 0, ans[i + 2 * m] = 0;
    ret = ask(ans);
    if (ret == q3 - 3) {
      out[i] = 0; out[i + m] = 1, out[i + 2 * m] = 1;
    } else if (ret == q3 + 3) {
      out[i] = 1; out[i + m] = 0; out[i + 2 * m] = 0;
    } else if (ret == q3 + 1) {
      out[i] = 1; out[i + m] = 0; out[i + 2 * m] = 1;
    } else if (ret == q3 - 1) {
      out[i] = 0; out[i + m] = 1; out[i + 2 * m] = 0;
    }
    ans[i] = 0, ans[i + m] = 1, ans[i + 2 * m] = 1;
  }
  std::fill(ans + 1, ans + 1 + n, 1);
  for (int i = 1; i <= n; i++) {
    if (out[i] != -1) continue;
    ans[i] = 0;
    ret = ask(ans);
    if (ret < num) out[i] = 1;
    else out[i] = 0;
    ans[i] = 1;
  }
  for (int i = 1; i <= n; i++)
    printf(out[i] == 1 ? "T" : "F");
  Enter;
  return 0;
}

CF1142E Pink Floyd

题目链接

好申必的题,可能是因为我水平不够吧……

为了解决这道题,首先我们要解决没有粉色边全部是绿色边的情况。
因为询问的操作次数上限是 \(2n\) 再加上交互库有自适应功能,所以找 \(u\) 再验证显然是不可能的。
我没有证明,也就感性理解一下好像没有什么大的问题。

然后发现一种神奇的做法:可以逐步去筛选那些可能是 \(u\) 的点,最后剩下的显然就是答案。
初始化时所有的点都在候选的集合里面,我们令这个集合为 \(S\)
枚举 \(u,v\in S\) 如果 \(u\rightarrow v\) 我们就把 \(v\)\(S\) 中删掉,否则把 \(u\)\(S\) 中删掉。
这个东西是非常显然的,因为我们假设令 \(P(x)\) 表示 \(x\) 点能到的所有点的集合,这个时候我们可以发现当 \(u\rightarrow v\) 的时候可以直接把 \(v\) 纳入到 \(P(u)\) 里面去,所以在不在 \(S\) 中就不那么必要了。
容易发现:每一次询问都会有一个点从 \(S\) 集合中删去,我们一共进行 \(n-1\) 此操作就可以使集合只剩下一个元素,也就是答案。

现在来考虑完整的题目:由数据的范围可以看出,只走粉边到达所有的点是不可能的事情,所以考虑从绿边下手。
同样的,我们还是要先确定一个可能的答案集合 \(S\) ,使得 \(S\) 中任意的两个点没有粉边相连,且满足集合内的点能通过一些构造的手段使得能和非集合中的点连边。
因为集合中是没有粉边的情况的,所以我们又回到了一开始 \(m=0\) 的问题上,可以直接调用构造的方法。

仔细分析上述筛选答案的方法,可以发现 \(S\) 中点的入读度一定是 \(0\) 所以我们可以维护一个拓扑排序。
每次弹出两个点,然后然后按照筛选的方法舍弃一个,另一个加入到队列中,最后剩下的就是 \(u\)
讲的不太明白,具体可以看一下代码。

询问的复杂度是 \(O(n+m)\) 级别的,没有压力。

点击查看代码
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <queue>

#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
#define Enter putchar('\n')
#define quad putchar(' ')

const int N = 5e5 + 5;

int n, m, ok[305][305], visit[N], in[N], deg[N];
std::vector <int> dis1[N], dis2[N];

inline int ask(int a, int b) {
  printf("? %d %d\n", a, b);
  fflush(stdout);
  int ret; scanf("%d", &ret);
  return ret;
}

inline void DFS(int now) {
  visit[now] = in[now] = 1;
  for (int t : dis1[now]) {
    if (in[t] == 0) {
      deg[t] ++;
      dis2[now].emplace_back(t);
    }
    if (visit[t] == 0) DFS(t);
  }
  in[now] = 0;
}

signed main(void) {
  scanf("%d %d", &n, &m);
  for (int i = 1, x, y; i <= m; i++) {
    scanf("%d %d", &x, &y);
    dis1[x].emplace_back(y);
  }
  for (int i = 1; i <= n; i++) {
    if (visit[i]) continue;
    DFS(i);
  }
  std::queue <int> que;
  for (int i = 1; i <= n; i++) 
    if (deg[i] == 0) que.emplace(i);
  while (que.size() > 1) {
    int u = que.front(); que.pop();
    int v = que.front(); que.pop();
    if (ask(u, v) == 0) std::swap(u, v);
    que.push(u);
    for (int t : dis2[v]) {
      deg[t] --;
      if (deg[t] == 0) que.emplace(t);
    }
  }
  printf("! %d\n", que.front());
  return 0;
}

给我点赞瞄,给我点赞谢谢喵。

posted @ 2022-08-12 11:09  Aonynation  阅读(165)  评论(0编辑  收藏  举报