[54]電容充電-位元組跳動2018秋

weixin_34208283發表於2018-11-08

1.題目描述

有一臺用電容組成的計算器,其中每個電容元件都有一個最大容量值(正整數)。
對於單個電容,有如下操作指令:
指令1:放電操作-把該電容當前電量值清零
指令2:充電操作-把該電容當前電量補充到其最大容量值
對於兩個電容A和B,有如下操作指令:
指令3:轉移操作-從A中儘可能多的將電量轉移到B,轉移不會有電量損失,如果能夠充滿B的最大容量,那剩餘的電量仍然會留在A中
現在已知有兩個電容,其最大容量分別為ab,其初始狀態都是電量值為0,希望通過一系列的操作可以使其中某個電容(無所謂哪一個)中的電量值等於c(c也是正整數),這一系列操作所用的最少指令條數記為M,如果無論如何操作,都不可能完成,則定義此時M=0
顯然對於每一組確定的abc,一定會有一個M與其對應。

  • 輸入描述:
    每組測試樣本的輸入格式為:
    第一行是一個正整數N
    從第二行開始,每行都是3個正整數依次組成一組abc,一共有N
  • 輸出描述:
    輸出為N行,每行列印每一組的對應的M
  • 資料範圍:
    N0<N<100
    abc
    0<abc<10^5(50%)
    0<abc<10^7(30%)
    0<abc<10^9(20%)
  • 輸入示例:
    2 
    3 4 2 
    2 3 4
    
  • 輸出示例:
    4
    0
    
  • 說明:
    對於(3,4,2),最少只需要4個指令即可完成:
    (設最大容量為3的是A號電容,另一個是B號電容)
    1. 充電A轉移A->B
    2. 充電A轉移A->B
      此時A中當前電量為2,操作完成,所以輸出4
      對於(2,3,4),顯然不可能完成,輸出0

2.題目解析

  1. 簡單情況
    1. c>ac>b,則顯然不可能完成,M0
    2. c=ac=b,則顯然一次操作可完成,M1
  2. 其他情況
    示例條件
    • A容量a=3
    • B容量b=4
    • 目標電量c=2

一共有兩種方式
小容量的先充電,然後轉移到大容量

操作 A電量(a=3) B電量(b=4)
初始狀態 0 0
A充電 3 0
A轉移到B 0 3
A充電 3 3
A轉移到B 2 4

指令條數M=4

大容量的先充電,然後轉移到小容量

操作 A電量(a=3) B電量(b=4)
初始狀態 0 0
B充電 0 4
B轉移到A 3 1
B放電 0 1
B轉移到A 1 0
B充電 1 4
B轉移到A 3 2

指令條數M=6

從小到大一定比從大到小指令少?


現在考慮剩餘情況,即c<Max(a,b)c≠Min(a,b)(條件1)
將電容A、B看作一個整體,整體電容只有充電和放電兩種操作。其中充電使整體總電量增加,放電使整體總電量減少,則系統總電量可以達到的數值為ax+by(ab為整數,且ab<0),其中xy中為正的表示充電,為負的表示放電。因為條件1,所以ab一定是一正一負,即ab<0
要使電量為c,則要滿足等式ax+by=c。根據裴蜀定理,等式可以滿足當且僅當gcd(a,b)|c,即a,b的最大公約數也是c的因子。

裴蜀/貝祖定理
若是整數,且,那麼對於任意的整數,都一定是的倍數,特別地,一定存在整數,使成立。

相反,若c mod gcd(a,b)≠0,則電量不能達到cM0
現在假設gcd(a,b)|c,即存在一正一負的整數x,y滿足等式ax+by=c,不妨設x>0,則整體電容的具體操作流程如下:

  1. 給電容A充電
  2. 電容A轉移到電容B
  3. 若電容B滿電,則其放空電量
  4. 如果電容A電量非空,則轉2
  5. 轉1重複執行整個過程,直到某時刻電容A或B中電量為c為止

先給容量大的充電(),還是先給容量小的充電()?
指令條數M = 轉移次數 + 充電次數 + 放電次數
轉移次數 = 充電次數 + 放電次數?

其電容操作次數有如下兩種情況: 假設(b>a)

  1. c>a(此時a<c<b),則最終是B中先有c的電量(A存不下),此時有x次充電,−y次放電,x−y次轉移(因為B中先有c的電量,每次A充電必然要轉移到B,每次B放電必然A非空由步驟4轉步驟2進行轉移操作),共2(x−y)次操作。
  2. c<a(此時c<ac<b),則最終是A中先有c的電量(反證法:若B中先有,因為ab都大於c,所以必然是A轉移c電量給空的B,此時A先有c電量,矛盾),此時有x次充電,−y−1次放電(當A中有c的電量時,必然是轉移給B後,A中剩餘c的電量,此時B中滿電量不用放電也滿足題目要求,所以公式計算的放電次數需要減1),x−y−1次轉移(即實際需要充電次數x加上實際需要放電次數−y−1),共2(x−y−1)次操作。

因此只需要最小化|x|+|y|。若使用擴充套件歐幾里德演算法求出任意一組解(x,y),則等式ax+by=c的全部整數解為:

找出其中距離0最近的兩組解(x,y)(一組x>0,另一組y>0),其中|x|+|y|最小的一組就是達到最小運算元的解(x,y),根據上面分析可得到最小運算元。

參見[歐幾里得演算法]與[擴充套件歐幾里得演算法]

3.參考答案

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

// 擴充套件歐幾里得演算法
LL egcd(LL a, LL b, LL &x, LL &y) {
  if (b == 0) {
    x = 1, y = 0;
    return a;
  }
  LL q = egcd(b, a % b, y, x);
  y -= a / b * x;
  return q;
}

int main() {
  int N = 0;
  scanf("%d",&N);
  while (N--) {
    LL a, b, c, x, y;
    scanf("%lld%lld%lld", &a,&b,&c);

    int d = egcd(a, b, x, y); // 這裡是ax+by=d
    // c大於A和B的容量,不能完成
    // 根據裴蜀/貝祖定理,c不是gcd(a,b)的倍數,不能完成
    if (c > a && c > b || c % d) {
      printf("0\n");
      continue;
    }

    // 剛好1步充滿A或者B
    if (c == a || c == b) {
      printf("1\n");
      continue;
    }

    // 假設a>0
    if (y > 0) swap(x, y), swap(a, b);
    
    // 使ax + by = c (d是c的因子,即對上面式子ax + by = d兩邊同時乘以c / d)
    x *= c / d; // 乘上c的約分
    y *= c / d;

    LL a2 = a / d; // a約分
    LL b2 = b / d; // b約分
    LL k = x / b2; // |x|+|y|最小的k?????
    x -= k * b2; // 使這組(x, y)最接近0(x > 0時的整數解)
    y += k * a2;
    LL res;
    if (c > a)
      res = 2 * (x - y);
    else
      res = 2 * (x - y - 1);
    // |x|+|y|最小????
    x -= b2;
    y += a2;
    if (c > b)
      res = min(res, 2 * (y - x));
    else
      res = min(res, 2 * (y - x - 1));
    printf("%d\n",res);
  }
  return 0;
}

相關文章