經過努力+作弊,我終於完成了leetcode通過率最低的一道題

plle發表於2021-04-05

前兩天刷leetcode的時候,突發奇想,leetcode中最難的一道題是什麼樣子的呢?

於是,我就將所有題目(https://leetcode-cn.com/problemset/all/ )按照通過率排了個序(中英文網站題目不同),找到了它(截止到目前

2021年4月5日,它的通過率依然是最低的):

題目描述如下:

請你實現三個 API append,addAll 和 multAll 來實現奇妙序列。
請實現 Fancy 類 :

  • Fancy() 初始化一個空序列物件。
  • void append(val) 將整數 val 新增在序列末尾。
  • void addAll(inc) 將所有序列中的現有數值都增加 inc 。
  • void multAll(m) 將序列中的所有現有數值都乘以整數 m 。
  • int getIndex(idx) 得到下標為 idx 處的數值(下標從 0 開始),並將結果對 109 + 7 取餘。
  • 如果下標大於等於序列的長度,請返回 -1 。

力扣(LeetCode)

具體描述,請參考:https://leetcode-cn.com/problems/fancy-sequence/

這裡,我們從題目給定的模板開始:

public class Fancy {

    public Fancy() {

    }
    
    public void Append(int val) {

    }
    
    public void AddAll(int inc) {

    }
    
    public void MultAll(int m) {

    }
    
    public int GetIndex(int idx) {

    }
}

/**
 * Your Fancy object will be instantiated and called as such:
 * Fancy obj = new Fancy();
 * obj.Append(val);
 * obj.AddAll(inc);
 * obj.MultAll(m);
 * int param_4 = obj.GetIndex(idx);
 */

我們可以看到,最後的註釋說明了程式碼的使用方法,以下再次引用程式碼時,我們將忽略它們。

看題目描述,需要我們提供一個類似於連結串列/陣列的功能,可以向集合末尾新增一個整數,可以對集合中所有元素執行加上一個屬或乘上一個數的操作,最後還能通過索引獲取到對應索引處的值。

嗯。。。

聽起來不是很難嘛,程式碼改成下面試試?

public class Fancy {
    private List<int> list = new List<int>();
    public Fancy() {

    }
    
    public void Append(int val) {
        list.Add(val);
    }
    
    public void AddAll(int inc) {
        int cnt = this.list.Count;
        for(int i = 0; i < cnt; i++) {
            this.list[i] += inc;
        }
    }
    
    public void MultAll(int m) {
        int cnt = this.list.Count;
        for(int i = 0; i < cnt; i++) {
            this.list[i] *= m;
        }
    }
    
    public int GetIndex(int idx) {
        if(idx < 0 || idx >= this.list.Count) {
            return -1;
        }
        return this.list[idx] % 1000000007;
    }
}

執行一下...

然後,報錯了...

得到的結果居然有負值?肯定是溢位了,再次調整程式碼,計算的時候,將運算數與結果設定為 long,改成如下形式:

public class Fancy {
    private List<int> list = new List<int>();
    public Fancy() {

    }
    
    public void Append(int val) {
        list.Add(val);
    }
    
    public void AddAll(int inc) {
        int cnt = this.list.Count;
        for(int i = 0; i < cnt; i++) {
            long val = this.list[i] + (long)inc;

            this.list[i] = (int)(val % 1000000007);
        }
    }
    
    public void MultAll(int m) {
        int cnt = this.list.Count;
        for(int i = 0; i < cnt; i++) {
            long val = this.list[i] * (long)m;

            this.list[i] = (int)(val % 1000000007);
        }
    }
    
    public int GetIndex(int idx) {
        if(idx < 0 || idx >= this.list.Count) {
            return -1;
        }

        return (this.list[idx] % 1000000007);
    }
}

再次執行,嗯,很好,不報錯了,但是...超時了:

這,還能怎麼辦?經過一晚上的思考,也沒有得到一個完美的答案,怎麼辦?第二天,突然有了新想法,既然每次呼叫 AddAll 或者 MultAll 的時候,都要計算所有元素,效能瓶頸會不會在這裡?

是不是隻有在 GetIndex 的時候再計算可以避免執行不必要的計算?但,怎麼做呢?我們有閉包啊,只要記錄所有的AddAll, MultAll 的呼叫,並使用閉包記錄其引數,這樣就可以只在呼叫 AddAll 或 MultAll 的時候建立閉包就好了。

於是,程式碼就變成了下面這個樣子:

public class Fancy
{
    private List<Entry> list = new List<Entry>();
    private List<Func<int, int>> funcs = new List<Func<int, int>>();

    public Fancy() { }

    public void Append(int val)
    {
        // 在這裡,我們只要將節點直接新增到
        // 資料列表中即可。
        // 由於在本節點新增之前的所有AddAll/MultAll
        // 呼叫,均不應該計算,所以這裡我們將需要呼叫
        // 的索引指向呼叫函式列表最後一個元素的後面。
        this.list.Add(new Entry
        {
            Val = val,
            StartIndex = funcs.Count
        });
    }

    public void AddAll(int inc)
    {
        // 由於可能在呼叫Append之前呼叫本方法,
        // 但是由於此時列表中沒有任何元素,所以
        // 應忽略呼叫。
        if (this.list.Count <= 0)
        {
            return;
        }
        // 這裡並不執行計算操作,只是建立了一個
        // 閉包,並將其新增到函式列表中。
        this.funcs.Add(val =>
        {
            long v = val + (long)inc;
            return (int)(v % 1000000007);
        });
    }

    public void MultAll(int m)
    {
        // 由於可能在呼叫Append之前呼叫本方法,
        // 但是由於此時列表中沒有任何元素,所以
        // 應忽略呼叫。
        if (this.list.Count <= 0)
        {
            return;
        }
        // 這裡並不執行計算操作,只是建立了一個
        // 閉包,並將其新增到函式列表中。
        this.funcs.Add(val =>
        {
            long v = val * (long)m;
            return (int)(v % 1000000007);
        });
    }

    public int GetIndex(int idx)
    {
        // 如果沒有任何元素,直接返回 -1。
        if (idx < 0 || idx >= this.list.Count)
        {
            return -1;
        }
        // 這裡,我們獲取到了要找的目標節點,但是由於
        // 節點儲存的值為計算之前的值,所以這裡需要從
        // StartIndex 索引處開始計算值,並獲取到結果。
        Entry entry = this.list[idx];
        int val = entry.Val;
        int cnt = this.funcs.Count;
        for (int i = entry.StartIndex; i < cnt; i++)
        {
            val = this.funcs[i](val);
        }
        return val;
    }

    private struct Entry
    {
        public int Val { get; set; }
        public int StartIndex { get; set; }
    }
}

努力了這麼久,再執行一下試試:

沒天理啊,還是超時...

但題目還是要做的,繼續修改程式碼,我們這裡雖然進行了延遲求值,但是終歸每次 GetIndex 都要計算的,能不能減少這裡的運算呢?

於是,每次計算之後,我都更新了StartIndex值,程式碼改成了這樣:

public class Fancy
{
    private List<Entry> list = new List<Entry>(10000);
    private List<Func<long, long>> funcs = 
        new List<Func<long, long>>(10000);

    public Fancy() { }

    public void Append(int val)
    {
        this.list.Add(new Entry
        {
            Value = val,
            LastIndex = funcs.Count
        });
    }

    public void AddAll(int inc)
    {
        if (this.list.Count <= 0)
        {
            return;
        }
        this.funcs.Add(val =>
        {
            long v = val + inc;
            if (v > 1000000007)
            {
                return v % 1000000007;
            }
            return v;
        });
    }

    public void MultAll(int m)
    {
        if (this.list.Count <= 0)
        {
            return;
        }
        this.funcs.Add(val =>
        {
            long v = val * m;
            if (v > 1000000007)
            {
                return v % 1000000007;
            }
            return v;
        });
    }

    public int GetIndex(int idx)
    {
        if (idx >= this.list.Count || idx < 0)
        {
            return -1;
        }

        Entry entry = this.list[idx];
        // 這裡我們判斷最後執行函式的索引,如果已經
        // 執行了所有要執行的函式,那麼 entry.Value
        // 存放的就是目標值,直接返回。
        if (entry.LastIndex >= this.funcs.Count)
        {
            return entry.Value;
        }
        // 如果需要計算,就執行所有計算,然後返回最後計算的值。
        long val = entry.Value;
        for (int i = entry.LastIndex; i < funcs.Count; i++)
        {
            val = funcs[i](val);
        }
        entry.LastIndex = funcs.Count;
        entry.Value = (int)val;
        return entry.Value;
    }

    private struct Entry
    {
        public int Value { get; set; }
        public int LastIndex { get; set; }
    }
}

經過多次修改,滿懷希望的再次執行,但是結果再次讓我失望,再一次的超時了。

真是讓人絕望...

好在,我想到了一個作弊的方法,把程式碼從C#改成了C語言...

typedef long (*fun)(long, long);

typedef struct {
  int size;
  int funSize;
  int indexes[100000];
  int vals[100000];
  fun funcs[100000];
  int ps[100000];
} Fancy;


Fancy* fancyCreate() {
  Fancy* fancy = (Fancy*)malloc(sizeof(Fancy));
  if (fancy) {
    memset(fancy, 0, sizeof(Fancy));
  }
  return fancy;
}

void fancyAppend(Fancy* obj, int val) {
  obj->vals[obj->size] = val;
  obj->indexes[obj->size] = obj->funSize;
  obj->size++;
}

long add(long val, long inc) {
  val += inc;
  if (val < 1000000007) {
    return val;
  }
  return val % 1000000007;
}

long mult(long val, long m) {
  val *= m;
  if (val < 1000000007) {
    return val;
  }
  return val % 1000000007;
}

void fancyAddAll(Fancy* obj, int inc) {
  obj->funcs[obj->funSize] = add;
  obj->ps[obj->funSize] = inc;
  obj->funSize++;
}

void fancyMultAll(Fancy* obj, int m) {
  obj->funcs[obj->funSize] = mult;
  obj->ps[obj->funSize] = m;
  obj->funSize++;
}

int fancyGetIndex(Fancy* obj, int idx) {
  if (idx >= obj->size) {
    return -1;
  }
  int last = obj->indexes[idx];
  long val = obj->vals[idx];
  if (last >= obj->funSize) {
    return (int)val;
  }

  while (last < obj->funSize) {
    val = obj->funcs[last](val, obj->ps[last]);
    last++;
  }
  obj->vals[idx] = val;
  obj->indexes[idx] = obj->funSize;
  return (int)val;
}

void fancyFree(Fancy* obj) {
  free(obj);
}

執行,然後,就通過了:

終於通過了,終於可以看答案了,又漲知識,原來這個是有演算法上的解決方法的,也貼到下面吧:

class Fancy {
private:
    static constexpr int mod = 1000000007;
    vector<int> v, a, b;
    
public:
    Fancy() {
        a.push_back(1);
        b.push_back(0);
    }
    
    // 快速冪
    int quickmul(int x, int y) {
        int ret = 1;
        int cur = x;
        while (y) {
            if (y & 1) {
                ret = (long long)ret * cur % mod;
            }
            cur = (long long)cur * cur % mod;
            y >>= 1;
        }
        return ret;
    }
    
    // 乘法逆元
    int inv(int x) {
        return quickmul(x, mod - 2);
    }
    
    void append(int val) {
        v.push_back(val);
        a.push_back(a.back());
        b.push_back(b.back());
    }
    
    void addAll(int inc) {
        b.back() = (b.back() + inc) % mod;
    }
    
    void multAll(int m) {
        a.back() = (long long)a.back() * m % mod;
        b.back() = (long long)b.back() * m % mod;
    }
    
    int getIndex(int idx) {
        if (idx >= v.size()) {
            return -1;
        }
        int ao = (long long)inv(a[idx]) * a.back() % mod;
        int bo = (b.back() - (long long)b[idx] * ao % mod + mod) % mod;
        int ans = ((long long)ao * v[idx] % mod + bo) % mod;
        return ans;
    }
};

作者:zerotrac2
連結:https://leetcode-cn.com/problems/fancy-sequence/solution/qi-miao-xu-lie-by-zerotrac2/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

至於具體的說明,就請移步leetcode檢視吧

相關文章