description: This is the hard version of the problem. The only difference between the two versions is the constraint on n. You can make hacks only if all versions of the problem are solved.
  
  A forest is an undirected graph without cycles (not necessarily connected).
  
  Mocha and Diana are friends in Zhijiang, both of them have a forest with nodes numbered from 1 to n, and they would like to add edges to their forests such that: 
  
    * After adding edges, both of their graphs are still forests. 
    * They add the same edges. That is, if an edge (u, v) is added to Mocha's forest, then an edge (u, v) is added to Diana's forest, and vice versa. 
  
  
  
  Mocha and Diana want to know the maximum number of edges they can add, and which edges to add.
  
  Input
  
  The first line contains three integers n, m_1 and m_2 (1 ≤ n ≤ 10^5, 0 ≤ m_1, m_2 < n) — the number of nodes and the number of initial edges in Mocha's forest and Diana's forest.
  
  Each of the next m_1 lines contains two integers u and v (1 ≤ u, v ≤ n, u ≠ v) — the edges in Mocha's forest.
  
  Each of the next m_2 lines contains two integers u and v (1 ≤ u, v ≤ n, u ≠ v) — the edges in Diana's forest.
  
  Output
  
  The first line contains only one integer h, the maximum number of edges Mocha and Diana can add.
  
  Each of the next h lines contains two integers u and v (1 ≤ u, v ≤ n, u ≠ v) — the edge you add each time.
  
  If there are multiple correct answers, you can print any one of them.
  
  Examples
  
  Input
  
  
  3 2 2
  1 2
  2 3
  1 2
  1 3
  
  
  Output
  
  
  0
  
  
  Input
  
  
  5 3 2
  5 4
  2 1
  4 3
  4 3
  1 4
  
  
  Output
  
  
  1
  2 4
  
  
  Input
  
  
  8 1 2
  1 7
  2 6
  1 5
  
  
  Output
  
  
  5
  5 2
  2 3
  3 4
  4 7
  6 8
  
  Note
  
  In the first example, we cannot add any edge.
  
  In the second example, the initial forests are as follows.
  
  <image>
  
  We can add an edge (2, 4).
  
  <image>
solution: 
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.util.*;
  
  
  
  import static javax.swing.UIManager.get;
  import static javax.swing.UIManager.getString;
  
  
  public class Main {
  
  
      static class Pair implements Comparable<Pair> {
          int x = 0;
          int y = 0;
  
          public Pair(int x1, int y1) {
              x = x1;
              y = y1;
          }
  
  
          @Override
          public int compareTo(Pair o) {
              return this.x - o.x;
          }
      }
  
      static boolean checkPallindrome(int n) {
          ArrayList<Integer> list = new ArrayList<>();
          while (n > 0) {
              list.add(n % 10);
              n /= 10;
          }
          int low = 0, high = list.size() - 1;
          while (low <= high) {
              if (list.get(low) != list.get(high))
                  return false;
              low++;
              high--;
          }
          return true;
      }
  
  
      static boolean check(String s) {
  
          int low = 0, high = s.length() - 1;
          while (low <= high) {
              if (s.charAt(low) != s.charAt(high))
                  return false;
              low++;
              high--;
          }
          return true;
      }
  
      class Node implements Comparable<Node> {
          int x = 0, y = 0, z = 0;
  
          @Override
          public int compareTo(Node o) {
              return this.y - o.y;
          }
      }
  
      static int min = Integer.MAX_VALUE;
  
      public static void main(String[] args) throws IOException {
          FastReader sc = new FastReader();
  //(1)very very important**(never take the first problem for granted, always check the test cases) take 5 minutes more and check the edge cases
          // 5 minutes will not decreases rating as much as a wrong submission does it is easy u just think with an open mind and u will surely get the answer
          //(2)let ur brain consume the problem don't just jump to the solution. after reading the problem take a pause 1 minute
          //(3)go through the example test cases and also at least two of ur own test cases.Think of testcases which are difficult(edge cases).dry run ur concept
   //(4) sometimes if else condition is not required but due to if else you miss some points and get wrong answer
  //        int t = sc.nextInt();
  //        while (t-- > 0) {
  //    }
  
          int n =sc.nextInt(),m1= sc.nextInt(),m2=sc.nextInt();
          DisjointUnionSets d1 = new DisjointUnionSets(n);
          DisjointUnionSets d2 = new DisjointUnionSets(n);
  
          ArrayList<Pair> list = new ArrayList<>();
  
          for(int i=0;i<m1;i++)
              d1.union(sc.nextInt()-1,sc.nextInt()-1);
  
          for(int i=0;i<m2;i++)
              d2.union(sc.nextInt()-1,sc.nextInt()-1);
  
          int count=0;
  
          for(int i=1;i<n;i++)
              if(d1.find(0)!= d1.find(i)&&d2.find(0)!=d2.find(i))
              {  list.add(new Pair(1,i+1));
                 d1.union(0,i);
                 d2.union(0,i);
                 count++;
              }
  
          Stack<Integer> stack1 = new Stack<>();
          Stack<Integer> stack2 = new Stack<>();
              for(int i=1;i<n;i++)
             if(d1.find(0)!=d1.find(i))
                 stack1.push(i);
  
          for(int i=1;i<n;i++)
              if(d2.find(0)!=d2.find(i))
                  stack2.push(i);
  
              while(!stack1.isEmpty()&&!stack2.isEmpty())
              {
                  while(!stack1.isEmpty()&&!stack2.isEmpty()&&d1.find(stack1.peek())==d1.find(0)&&d2.find(stack1.peek())==d2.find(0))
                      stack1.pop();
                  if(stack1.isEmpty()||stack2.isEmpty())
                      break;
                  while(!stack1.isEmpty()&&!stack2.isEmpty()&&d1.find(stack2.peek())==d1.find(0)&&d2.find(stack2.peek())==d2.find(0))
                      stack2.pop();
  
                  if(stack1.isEmpty()||stack2.isEmpty())
                      break;
  
                  if(!stack1.isEmpty()&&!stack2.isEmpty())
                  {  d1.union(stack1.peek(),stack2.peek());
                      d2.union(stack1.peek(),stack2.peek());
                      list.add(new Pair(stack1.pop()+1,stack2.pop()+1));
                      count++;
                  }
  
              }
              System.out.println(count);
              for(Pair x:list)
              System.out.println(x.x+" "+x.y);
  
  }
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  // syntax of conditional operator y=(x==1)?1:0;
  //Things to check when u r getting wrong answer
  // 1-  check the flow of the code
  //2- If ur stuck read the problem once again
  //3- before submitting always check the output format of ur code
  //4- don't check standings until problem B is done
  //5- if u r thinking ur concept is correct but still u r getting wrong answer try to implement it in another way
  //6- By default, java interpret all numeral literals as 32-bit integer values.
  // If you want to explicitely specify that this is something bigger then 32-bit integer you should use suffix L for long values.    example   long a = 600851475143L
  
  //All the functions
  
  
  
  
      long pow(long a,long b,long m)
      {
          a%=m;
          long res=1L;
          while(b>0)
          {
              long temp=b&1;
              if(temp==1)
                  res=(res*a)%m;
              a=(a*a)%m;
              b>>=1;
          }
          return res;
      }
  
      static class DisjointUnionSets {
          int[] rank, parent;
          int n;
  
          // Constructor
          public DisjointUnionSets(int n)
          {
              rank = new int[n];
              parent = new int[n];
              this.n = n;
              makeSet();
          }
  
          // Creates n sets with single item in each
          void makeSet()
          {
              for (int i = 0; i < n; i++) {
                  // Initially, all elements are in
                  // their own set.
                  parent[i] = i;
              }
          }
  
          // Returns representative of x's set
          int find(int x)
          {
              // Finds the representative of the set
              // that x is an element of
              if (parent[x] != x) {
                  // if x is not the parent of itself
                  // Then x is not the representative of
                  // his set,
                  parent[x] = find(parent[x]);
  
                  // so we recursively call Find on its parent
                  // and move i's node directly under the
                  // representative of this set
              }
  
              return parent[x];
          }
  
          // Unites the set that includes x and the set
          // that includes x
          void union(int x, int y)
          {
              // Find representatives of two sets
              int xRoot = find(x), yRoot = find(y);
  
              // Elements are in the same set, no need
              // to unite anything.
              if (xRoot == yRoot)
                  return;
  
              // If x's rank is less than y's rank
              if (rank[xRoot] < rank[yRoot])
  
                  // Then move x under y  so that depth
                  // of tree remains less
                  parent[xRoot] = yRoot;
  
                  // Else if y's rank is less than x's rank
              else if (rank[yRoot] < rank[xRoot])
  
                  // Then move y under x so that depth of
                  // tree remains less
                  parent[yRoot] = xRoot;
  
              else // if ranks are the same
              {
                  // Then move y under x (doesn't matter
                  // which one goes where)
                  parent[yRoot] = xRoot;
  
                  // And increment the result tree's
                  // rank by 1
                  rank[xRoot] = rank[xRoot] + 1;
              }
          }
      }
  
  
  
  
  
  
  
  
  
  
      static void debug(String s)
      { System.out.println(s);
  
      }
  
      //collections.sort use merge sort instead of quick sort but arrays.sort use quicksort whose worst time complexity is O(n^2)
      static int[] sort(int[] a)
      {         ArrayList<Integer> list = new ArrayList<>();
          for(int i=0;i<a.length;i++)
              list.add(a[i]);
  
          Collections.sort(list);
          int ind=0;
          for(int x:list)
              a[ind++]=x;
          return a;
  
      }
  
      //function to print an array for debugging
      static void print(int[] a) {
          for (int i = 0; i < a.length; i++)
              System.out.print(a[i] + " ");
          System.out.println();
      }
  
      static void printc(char[] a) {
          for (int i = 0; i < a.length; i++)
              System.out.print(a[i] + " ");
          System.out.println();
      }
  
  
      //normal gcd function, always put the greater number as a and the smaller number as b
      static int gcd(int a, int b) {
          if (b == 0)
              return a;
          return gcd(b, a % b);
  
      }
  
      static int lcm(int a,int b)
      {
          return (a*b)/gcd(a,b);
      }
  
  
      //to find gcd and lcm for numbers of long data type
      static long gcdl(long a, long b) {
          if (b == 0)
              return a;
          return gcdl(b, a % b);
  
      }
  
      static long lcml(long a,long b)
      {
          return (a*b)/gcdl(a,b);
      }
  
  
      //Input Reader to read faster input
      static class FastReader {
          BufferedReader br;
          StringTokenizer st;
  
          public FastReader() {
              br = new BufferedReader(new
                      InputStreamReader(System.in));
          }
  
          String next() {
              while (st == null || !st.hasMoreElements()) {
                  try {
                      st = new StringTokenizer(br.readLine());
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              return st.nextToken();
          }
  
          int nextInt() {
              return Integer.parseInt(next());
          }
  
          long nextLong() {
              return Long.parseLong(next());
          }
  
          double nextDouble() {
              return Double.parseDouble(next());
          }
  
          String nextLine() {
              String str = "";
              try {
                  str = br.readLine();
              } catch (IOException e) {
                  e.printStackTrace();
              }
              return str;
          }
  
  
          int[] readArray(int n)
          {  int[] a = new int[n];
              for(int i=0;i<n;i++)
                  a[i]=nextInt();
  
              return a;
          }
      }
  
  
  
  }
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  