1. 程式人生 > 其它 >動態規劃— —提高Ⅳ

動態規劃— —提高Ⅳ

區間DP

一、問題

給定長為n的序列a[i],每次可以將連續一段迴文序列消去,消去後左右兩邊會接到一起,求最少消幾次能消完整個序列,n≤500。

f[i][j]表示消去區間[i,j]需要的最少次數。

若a[i]=a[j],則還有

這裡實際上是以區間長度為階段的,這種DP我們通常稱為區間DP。

區間DP的做法較為固定,即列舉區間長度,再列舉左端點,之後列舉區間的斷點進行轉移。

二、概念

區間型別動態規劃是線性動態規劃的拓展,它在分階段劃分問題時,與階段中元素出現的順序和由前一階段的哪些元素合併而來有很大的關係。(例:f[i][j]=f[i][k]+f[k+1][j]

區間類動態規劃的特點:

合併:即將兩個或多個部分進行整合。

特徵:能將問題分解成為兩兩合併的形式。

求解:對整個問題設最優值,列舉合併點,將問題分解成為左右兩個部分,最後將左右兩個部分的最優值進行合併得到原問題的最優值。

三.常見解題思路

一、區間DP解題時常見思路
如果題目中答案滿足:

  • 大的區間的答案可以由小的區間答案組合或加減得到

  • 大的範圍可以由小的範圍代表

  • 資料範圍較小

我們這時可以考慮採用區間DP來解決。

那麼常見的解法有兩種:

1.用小的區間組合鬆弛大的區間,即列舉斷點,分割區間,與答案取優。

2.用比當前區間略小的區間轉移,用一些區間邊界代表轉移用的性質,通過常數的加減得到答案。

四.程式碼實現

將 n 堆石子繞圓形操場排放,現要將石子有序地合併成一堆。

規定每次只能選相鄰的兩堆合併成新的一堆,並將新的一堆的石子數記做該次合併的得分。

請編寫一個程式,讀入堆數 n 及每堆的石子數,並進行如下計算:

選擇一種合併石子的方案,使得做 n−1 次合併得分總和最大。
選擇一種合併石子的方案,使得做 n−1 次合併得分總和最小。
輸入格式
第一行包含整數 n,表示共有 n 堆石子。

第二行包含 n 個整數,分別表示每堆石子的數量。

輸出格式
輸出共兩行:

第一行為合併得分總和最小值,

第二行為合併得分總和最大值。

資料範圍
1≤n≤200
輸入樣例:
4
4 5 9 4
輸出樣例:
43
54

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 410, INF = 0x3f3f3f3f;

int n;
int w[N], s[N];
int f[N][N], g[N][N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> w[i];
        w[i + n] = w[i];
    }

    for (int i = 1; i <= n * 2; i ++ ) s[i] = s[i - 1] + w[i];

    memset(f, 0x3f, sizeof f);
    memset(g, -0x3f, sizeof g);

    for (int len = 1; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n * 2; l ++ )
        {
            int r = l + len - 1;
            if (l == r) f[l][r] = g[l][r] = 0;
            else
            {
                for (int k = l; k < r; k ++ )
                {
                    f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
                    g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
                }
            }
        }

    int minv = INF, maxv = -INF;
    for (int i = 1; i <= n; i ++ )
    {
        minv = min(minv, f[i][i + n - 1]);
        maxv = max(maxv, g[i][i + n - 1]);
    }

    cout << minv << endl << maxv << endl;

    return 0;
}


在 Mars 星球上,每個 Mars 人都隨身佩帶著一串能量項鍊,在項鍊上有 N 顆能量珠。

能量珠是一顆有頭標記與尾標記的珠子,這些標記對應著某個正整數。

並且,對於相鄰的兩顆珠子,前一顆珠子的尾標記一定等於後一顆珠子的頭標記。

因為只有這樣,通過吸盤(吸盤是 Mars 人吸收能量的一種器官)的作用,這兩顆珠子才能聚合成一顆珠子,同時釋放出可以被吸盤吸收的能量。

如果前一顆能量珠的頭標記為 m,尾標記為 r,後一顆能量珠的頭標記為 r,尾標記為 n,則聚合後釋放的能量為 m×r×n(Mars 單位),新產生的珠子的頭標記為 m,尾標記為 n。

需要時,Mars 人就用吸盤夾住相鄰的兩顆珠子,通過聚合得到能量,直到項鍊上只剩下一顆珠子為止。

顯然,不同的聚合順序得到的總能量是不同的,請你設計一個聚合順序,使一串項鍊釋放出的總能量最大。

例如:設 N=4,4 顆珠子的頭標記與尾標記依次為 (2,3)(3,5)(5,10)(10,2)。

我們用記號 ⊕ 表示兩顆珠子的聚合操作,(j⊕k) 表示第 j,k 兩顆珠子聚合後所釋放的能量。則

第 4、1 兩顆珠子聚合後釋放的能量為:(4⊕1)=10×2×3=60。

這一串項鍊可以得到最優值的一個聚合順序所釋放的總能量為 ((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。

輸入格式
輸入的第一行是一個正整數 N,表示項鍊上珠子的個數。

第二行是 N 個用空格隔開的正整數,所有的數均不超過 1000,第 i 個數為第 i 顆珠子的頭標記,當 i<N 時,第 i 顆珠子的尾標記應該等於第 i+1 顆珠子的頭標記,第 N 顆珠子的尾標記應該等於第 1 顆珠子的頭標記。

至於珠子的順序,你可以這樣確定:將項鍊放到桌面上,不要出現交叉,隨意指定第一顆珠子,然後按順時針方向確定其他珠子的順序。

輸出格式
輸出只有一行,是一個正整數 E,為一個最優聚合順序所釋放的總能量。

資料範圍
4≤N≤100,
1≤E≤2.1×10^9
輸入樣例:
4
2 3 5 10
輸出樣例:
710

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 0x3f3f3f3f;

int n;
int w[N];
int f[N][N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> w[i];
        w[i + n] = w[i];
    }

    for (int len = 3; len <= n + 1; len ++ )
        for (int l = 1; l + len - 1 <= n * 2; l ++ )
        {
            int r = l + len - 1;
            for (int k = l + 1; k < r; k ++ )
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
        }

    int res = 0;
    for (int l = 1; l <= n; l ++ ) res = max(res, f[l][l + n]);

    cout << res << endl;

    return 0;
}


設一個 n 個節點的二叉樹 tree 的中序遍歷為(1,2,3,…,n),其中數字 1,2,3,…,n 為節點編號。

每個節點都有一個分數(均為正整數),記第 i 個節點的分數為 di,tree 及它的每個子樹都有一個加分,任一棵子樹 subtree(也包含 tree 本身)的加分計算方法如下:     

subtree的左子樹的加分 × subtree的右子樹的加分 + subtree的根的分數 

若某個子樹為空,規定其加分為 1。

葉子的加分就是葉節點本身的分數,不考慮它的空子樹。

試求一棵符合中序遍歷為(1,2,3,…,n)且加分最高的二叉樹 tree。

要求輸出: 

(1)tree的最高加分 

(2)tree的前序遍歷

輸入格式
第 1 行:一個整數 n,為節點個數。 

第 2 行:n 個用空格隔開的整數,為每個節點的分數(0<分數<100)。

輸出格式
第 1 行:一個整數,為最高加分(結果不會超過int範圍)。     

第 2 行:n 個用空格隔開的整數,為該樹的前序遍歷。如果存在多種方案,則輸出字典序最小的方案。

資料範圍
n<30
輸入樣例:
5
5 7 1 2 10
輸出樣例:
145
3 1 2 4 5

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 30;

int n;
int w[N];
int f[N][N], g[N][N];

void dfs(int l, int r)
{
    if (l > r) return;
    int k = g[l][r];
    cout << k << ' ';
    dfs(l, k - 1);
    dfs(k + 1, r);
}

int main()
{
    cin >> n;

    for (int i = 1; i <= n; i ++ ) cin >> w[i];

    for (int len = 1; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n; l ++ )
        {
            int r = l + len - 1;
            if (len == 1) f[l][r] = w[l], g[l][r] = l;
            else
            {
                for (int k = l; k <= r; k ++ )
                {
                    int left = k == l ? 1 : f[l][k - 1];
                    int right = k == r ? 1 : f[k + 1][r];
                    int score = left * right + w[k];
                    if (f[l][r] < score)
                    {
                        f[l][r] = score;
                        g[l][r] = k;
                    }
                }
            }
        }

    cout << f[1][n] << endl;

    dfs(1, n);

    return 0;
}


給定一個具有 N 個頂點的凸多邊形,將頂點從 1 至 N 標號,每個頂點的權值都是一個正整數。

將這個凸多邊形劃分成 N−2 個互不相交的三角形,對於每個三角形,其三個頂點的權值相乘都可得到一個權值乘積,試求所有三角形的頂點權值乘積之和至少為多少。

輸入格式
第一行包含整數 N,表示頂點數量。

第二行包含 N 個整數,依次為頂點 1 至頂點 N 的權值。

輸出格式
輸出僅一行,為所有三角形的頂點權值乘積之和的最小值。

資料範圍
N≤50,
資料保證所有頂點的權值都小於109
輸入樣例:
5
121 122 123 245 231
輸出樣例:
12214884

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 55, M = 35, INF = 1e9;

int n;
int w[N];
LL f[N][N][M];

void add(LL a[], LL b[])
{
    static LL c[M];
    memset(c, 0, sizeof c);
    for (int i = 0, t = 0; i < M; i ++ )
    {
        t += a[i] + b[i];
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}

void mul(LL a[], LL b)
{
    static LL c[M];
    memset(c, 0, sizeof c);
    LL t = 0;
    for (int i = 0; i < M; i ++ )
    {
        t += a[i] * b;
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}

int cmp(LL a[], LL b[])
{
    for (int i = M - 1; i >= 0; i -- )
        if (a[i] > b[i]) return 1;
        else if (a[i] < b[i]) return -1;
    return 0;
}

void print(LL a[])
{
    int k = M - 1;
    while (k && !a[k]) k -- ;
    while (k >= 0) cout << a[k -- ];
    cout << endl;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ ) cin >> w[i];

    LL temp[M];
    for (int len = 3; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n; l ++ )
        {
            int r = l + len - 1;
            f[l][r][M - 1] = 1;
            for (int k = l + 1; k < r; k ++ )
            {
                memset(temp, 0, sizeof temp);
                temp[0] = w[l];
                mul(temp, w[k]);
                mul(temp, w[r]);
                add(temp, f[l][k]);
                add(temp, f[k][r]);
                if (cmp(f[l][r], temp) > 0)
                    memcpy(f[l][r], temp, sizeof temp);
            }
        }

    print(f[1][n]);

    return 0;
}


將一個 8×8 的棋盤進行如下分割:將原棋盤割下一塊矩形棋盤並使剩下部分也是矩形,再將剩下的部分繼續如此分割,這樣割了 (n−1) 次後,連同最後剩下的矩形棋盤共有 n 塊矩形棋盤。(每次切割都只能沿著棋盤格子的邊進行)


原棋盤上每一格有一個分值,一塊矩形棋盤的總分為其所含各格分值之和。

現在需要把棋盤按上述規則分割成 n 塊矩形棋盤,並使各矩形棋盤總分的均方差最小。

均方差formula.png ,其中平均值lala.png ,xi 為第 i 塊矩形棋盤的總分。

請程式設計對給出的棋盤及 n,求出均方差的最小值。

輸入格式
第 1 行為一個整數 n。

第 2 行至第 9 行每行為 8 個小於 100 的非負整數,表示棋盤上相應格子的分值。每行相鄰兩數之間用一個空格分隔。

輸出格式
輸出最小均方差值(四捨五入精確到小數點後三位)。

資料範圍
1<n<15
輸入樣例:
3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3
輸出樣例:
1.633
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 15, M = 9;
const double INF = 1e9;

int n, m = 8;
int s[M][M];
double f[M][M][M][M][N];
double X;

int get_sum(int x1, int y1, int x2, int y2)
{
    return s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
}

double get(int x1, int y1, int x2, int y2)
{
    double sum = get_sum(x1, y1, x2, y2) - X;
    return (double)sum * sum / n;
}

double dp(int x1, int y1, int x2, int y2, int k)
{
    double &v = f[x1][y1][x2][y2][k];
    if (v >= 0) return v;
    if (k == 1) return v = get(x1, y1, x2, y2);

    v = INF;
    for (int i = x1; i < x2; i ++ )
    {
        v = min(v, get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1));
        v = min(v, get(i + 1, y1, x2, y2) + dp(x1, y1, i, y2, k - 1));
    }

    for (int i = y1; i < y2; i ++ )
    {
        v = min(v, get(x1, y1, x2, i) + dp(x1, i + 1, x2, y2, k - 1));
        v = min(v, get(x1, i + 1, x2, y2) + dp(x1, y1, x2, i, k - 1));
    }

    return v;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= m; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
        }

    X = (double)s[m][m] / n;
    memset(f, -1, sizeof f);
    printf("%.3lf\n", sqrt(dp(1, 1, 8, 8, n)));

    return 0;
}


樹形DP

一、概念

1、什麼是樹型動態規劃

樹型動態規劃就是在“樹”的資料結構上的動態規劃,平時作的動態規劃都是線性的或者是建立在圖上的,線性的動態規劃有二種方向既向前和向後,相應的線性的動態規劃有二種方法既順推與逆推,而樹型動態規劃是建立在樹上的,所以也相應的有二個方向:

葉->根:在回溯的時候從葉子節點往上更新資訊

根 - >葉:往往是在從葉往根dfs一遍之後(相當於預處理),再重新往下獲取最後的答案。

不管是 從葉->根 還是 從 根 - >葉,兩者都是根據需要採用,沒有好壞高低之分。

2、樹真的是一種特別特別優美的結構!

用來做動態規劃也簡直是錦上添花再美不過的事,因為樹本身至少就有“子結構”性質(樹和子樹);本身就具有遞迴性。所以在樹上DP其實是其所當然的事,相比線性動態規劃來講,轉移方程更直觀,更易理解。

3、難點

和線性動態規劃相比,樹形DP往往是要利用遞迴+記憶化搜尋。

細節多,較為複雜的樹形DP,從子樹,從父親,從兄弟……一些小的要處理的地方,腦子不清晰的時候做起來頗為噁心
狀態表示和轉移方程,也是真正難的地方。做到後面,樹形DP的老套路都也就那麼多,難的還是怎麼能想出轉移方程,各種DP做到最後都是這樣!

4.解題規律總結

一般來說樹形dp在設狀態轉移方程時都可以用f[i][]表示i這顆子樹怎麼怎麼樣的最優解,實現時一般都是用子樹更新父親(即從下向上更新),那麼首先應該考慮的是一個一個子樹的更新父親還是把所有子樹都算完了在更新父親?這就要因題而異了,一般來說有兩種情況:

  • 1.需要把所有子樹的資訊都掌握之後再更新子樹的就需要把所有子樹都算完了在更新父親。

  • 2.而像樹上揹包這樣的問題就需要一個一個的更新,每次都用一個子樹更新已經更新完的子樹+父親,最後就可以將這一部分的子樹更新完了,再繼續往上更新,最後根節點就是答案。

其實上面的兩種情況可以總結成一種情況就是一個個子樹更新父親,一般來說第一種情況應用更多,也能解決第二情況的問題,只不過如果符合第二種情況的時候用第二種可以速度更快一點,畢竟你省了一遍迴圈嘛。

在分題型說說說說樹形dp的規律!

子樹和計數。

這類問題主要是統計子樹和,通過加減一些子樹滿足題目中要求的某些性質。

樹上揹包問題

這類問題就是讓你求在樹上選一些點滿足價值最大的問題,一般都可以設f[i][j]表示i這顆子樹選j個點的最優解。

花費最少的費用覆蓋所有點

這類問題是父親與孩子有聯絡的題。基本有兩種出法:1.選父親必須不能選孩子(強制)2.選父親可以不用選孩子(不強制)。

二.找樹的直徑小技巧

找樹的直徑

1.任取一點作為起點,找到距離該點最遠的一個點u。(dfs,BFS)

2.再找到距離點u最遠的一點v。(dfs,BFS)

那麼u和v之間的路徑就是這條樹的直徑

三.程式碼實現

給定一棵樹,樹中包含 n 個結點(編號1~n)和 n−1 條無向邊,每條邊都有一個權值。

現在請你找到樹中的一條最長路徑。

換句話說,要找到一條路徑,使得使得路徑兩端的點的距離最遠。

注意:路徑中可以只包含一個點。

輸入格式
第一行包含整數 n。

接下來 n−1 行,每行包含三個整數 ai,bi,ci,表示點 ai 和 bi 之間存在一條權值為 ci 的邊。

輸出格式
輸出一個整數,表示樹的最長路徑的長度。

資料範圍
1≤n≤10000,
1≤ai,bi≤n,
−105≤ci≤105
輸入樣例:
6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7
輸出樣例:
22

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010, M = N * 2;

int n;
int h[N], e[M], w[M], ne[M], idx;
int ans;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int dfs(int u, int father)
{
    int dist = 0; // 表示從當前點往下走的最大長度
    int d1 = 0, d2 = 0;

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (j == father) continue;
        int d = dfs(j, u) + w[i];
        dist = max(dist, d);

        if (d >= d1) d2 = d1, d1 = d;
        else if (d > d2) d2 = d;
    }

    ans = max(ans, d1 + d2);

    return dist;
}

int main()
{
    cin >> n;

    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }

    dfs(1, -1);

    cout << ans << endl;

    return 0;
}


給定一棵樹,樹中包含 n 個結點(編號1~n)和 n−1 條無向邊,每條邊都有一個權值。

請你在樹中找到一個點,使得該點到樹中其他結點的最遠距離最近。

輸入格式
第一行包含整數 n。

接下來 n−1 行,每行包含三個整數 ai,bi,ci,表示點 ai 和 bi 之間存在一條權值為 ci 的邊。

輸出格式
輸出一個整數,表示所求點到樹中其他結點的最遠距離。

資料範圍
1≤n≤10000,
1≤ai,bi≤n,
1≤ci≤105
輸入樣例:
5 
2 1 1 
3 2 1 
4 3 1 
5 1 1
輸出樣例:
2

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010, M = N * 2, INF = 0x3f3f3f3f;

int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], p1[N], up[N];
bool is_leaf[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int dfs_d(int u, int father)
{
    d1[u] = d2[u] = -INF;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (j == father) continue;
        int d = dfs_d(j, u) + w[i];
        if (d >= d1[u])
        {
            d2[u] = d1[u], d1[u] = d;
            p1[u] = j;
        }
        else if (d > d2[u]) d2[u] = d;
    }

    if (d1[u] == -INF)
    {
        d1[u] = d2[u] = 0;
        is_leaf[u] = true;
    }

    return d1[u];
}

void dfs_u(int u, int father)
{
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (j == father) continue;

        if (p1[u] == j) up[j] = max(up[u], d2[u]) + w[i];
        else up[j] = max(up[u], d1[u]) + w[i];

        dfs_u(j, u);
    }
}

int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }

    dfs_d(1, -1);
    dfs_u(1, -1);

    int res = d1[1];
    for (int i = 2; i <= n; i ++ )
        if (is_leaf[i]) res = min(res, up[i]);
        else res = min(res, max(d1[i], up[i]));

    printf("%d\n", res);

    return 0;
}


如果一個數 x 的約數之和 y(不包括他本身)比他本身小,那麼 x 可以變成 y,y 也可以變成 x。

例如,4 可以變為 3,1 可以變為 7。

限定所有數字變換在不超過 n 的正整數範圍內進行,求不斷進行數字變換且不出現重複數字的最多變換步數。

輸入格式
輸入一個正整數 n。

輸出格式
輸出不斷進行數字變換且不出現重複數字的最多變換步數。

資料範圍
1≤n≤50000
輸入樣例:
7
輸出樣例:
3
樣例解釋
一種方案為:4→3→1→7。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 50010, M = N;

int n;
int h[N], e[M], w[M], ne[M], idx;
int sum[N];
bool st[N];
int ans;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int dfs(int u)
{
    st[u] = true;

    int dist = 0;
    int d1 = 0, d2 = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
       int j = e[i];
       if (!st[j])
       {
           int d = dfs(j);
           dist = max(dist, d);
           if (d >= d1) d2 = d1, d1 = d;
           else if (d > d2) d2 = d;
       }
    }

    ans = max(ans, d1 + d2);

    return dist + 1;
}

int main()
{
    cin >> n;
    memset(h, -1, sizeof h);

    for (int i = 1; i <= n; i ++ )
        for (int j = 2; j <= n / i; j ++ )
            sum[i * j] += i;

    for (int i = 2; i <= n; i ++ )
        if (sum[i] < i)
            add(sum[i], i);

    for (int i = 1; i <= n; i ++ )
        if (!st[i])
            dfs(i);

    cout << ans << endl;

    return 0;
}


有一棵二叉蘋果樹,如果樹枝有分叉,一定是分兩叉,即沒有隻有一個兒子的節點。

這棵樹共 N 個節點,編號為 1 至 N,樹根編號一定為 1。

我們用一根樹枝兩端連線的節點編號描述一根樹枝的位置。

一棵蘋果樹的樹枝太多了,需要剪枝。但是一些樹枝上長有蘋果,給定需要保留的樹枝數量,求最多能留住多少蘋果。

這裡的保留是指最終與1號點連通。

輸入格式
第一行包含兩個整數 N 和 Q,分別表示樹的節點數以及要保留的樹枝數量。

接下來 N−1 行描述樹枝資訊,每行三個整數,前兩個是它連線的節點的編號,第三個數是這根樹枝上蘋果數量。

輸出格式
輸出僅一行,表示最多能留住的蘋果的數量。

資料範圍
1≤Q<N≤100.
N≠1,
每根樹枝上蘋果不超過 30000 個。

輸入樣例:
5 2
1 3 1
1 4 10
2 3 20
3 5 20
輸出樣例:
21
           
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110, M = N * 2;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int f[N][N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u, int father)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        if (e[i] == father) continue;
        dfs(e[i], u);
        for (int j = m; j; j -- )
            for (int k = 0; k + 1 <= j; k ++ )
                f[u][j] = max(f[u][j], f[u][j - k - 1] + f[e[i]][k] + w[i]);
    }
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }

    dfs(1, -1);

    printf("%d\n", f[1][m]);

    return 0;
}


鮑勃喜歡玩電腦遊戲,特別是戰略遊戲,但有時他找不到解決問題的方法,這讓他很傷心。

現在他有以下問題。

他必須保護一座中世紀城市,這條城市的道路構成了一棵樹。

每個節點上的士兵可以觀察到所有和這個點相連的邊。

他必須在節點上放置最少數量的士兵,以便他們可以觀察到所有的邊。

你能幫助他嗎?

例如,下面的樹:

只需要放置 1 名士兵(在節點 1 處),就可觀察到所有的邊。

輸入格式
輸入包含多組測試資料,每組測試資料用以描述一棵樹。

對於每組測試資料,第一行包含整數 N,表示樹的節點數目。

接下來 N 行,每行按如下方法描述一個節點。

節點編號:(子節點數目) 子節點 子節點 …

節點編號從 0 到 N−1,每個節點的子節點數量均不超過 10,每個邊在輸入資料中只出現一次。

輸出格式
對於每組測試資料,輸出一個佔據一行的結果,表示最少需要的士兵數。

資料範圍
0<N≤1500,
一個測試點所有 N 相加之和不超過 300650。

輸入樣例:
4
0:(1) 1
1:(2) 2 3
2:(0)
3:(0)
5
3:(3) 1 4 2
1:(1) 0
2:(0)
0:(0)
4:(0)
輸出樣例:
1
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1510;

int n;
int h[N], e[N], ne[N], idx;
int f[N][2];
bool st[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][0] = 0, f[u][1] = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += f[j][1];
        f[u][1] += min(f[j][0], f[j][1]);
    }
}

int main()
{
    while (cin >> n)
    {
        memset(h, -1, sizeof h);
        idx = 0;

        memset(st, 0, sizeof st);
        for (int i = 0; i < n; i ++ )
        {
            int id, cnt;
            scanf("%d:(%d)", &id, &cnt);
            while (cnt -- )
            {
                int ver;
                cin >> ver;
                add(id, ver);
                st[ver] = true;
            }
        }

        int root = 0;
        while (st[root]) root ++ ;
        dfs(root);

        printf("%d\n", min(f[root][0], f[root][1]));
    }

    return 0;
}


太平王世子事件後,陸小鳳成了皇上特聘的御前一品侍衛。

皇宮各個宮殿的分佈,呈一棵樹的形狀,宮殿可視為樹中結點,兩個宮殿之間如果存在道路直接相連,則該道路視為樹中的一條邊。

已知,在一個宮殿鎮守的守衛不僅能夠觀察到本宮殿的狀況,還能觀察到與該宮殿直接存在道路相連的其他宮殿的狀況。

大內保衛森嚴,三步一崗,五步一哨,每個宮殿都要有人全天候看守,在不同的宮殿安排看守所需的費用不同。

可是陸小鳳手上的經費不足,無論如何也沒法在每個宮殿都安置留守侍衛。

幫助陸小鳳佈置侍衛,在看守全部宮殿的前提下,使得花費的經費最少。

輸入格式
輸入中資料描述一棵樹,描述如下:

第一行 n,表示樹中結點的數目。

第二行至第 n+1 行,每行描述每個宮殿結點資訊,依次為:該宮殿結點標號 i,在該宮殿安置侍衛所需的經費 k,該結點的子結點數 m,接下來 m 個數,分別是這個結點的 m 個子結點的標號 r1,r2,…,rm。

對於一個 n 個結點的樹,結點標號在 1 到 n 之間,且標號不重複。

輸出格式
輸出一個整數,表示最少的經費。

資料範圍
1≤n≤1500
輸入樣例:
6
1 30 3 2 3 4
2 16 2 5 6
3 5 0
4 4 0
5 11 0
6 5 0
輸出樣例:
25
樣例解釋:
在2、3、4結點安排護衛,可以觀察到全部宮殿,所需經費最少,為 16 + 5 + 4 = 25。


#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1510;

int n;
int h[N], w[N], e[N], ne[N], idx;
int f[N][3];
bool st[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][2] = w[u];

    int sum = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += min(f[j][1], f[j][2]);
        f[u][2] += min(min(f[j][0], f[j][1]), f[j][2]);
        sum += min(f[j][1], f[j][2]);
    }

    f[u][1] = 1e9;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        f[u][1] = min(f[u][1], sum - min(f[j][1], f[j][2]) + f[j][2]);
    }
}

int main()
{
    cin >> n;

    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i ++ )
    {
        int id, cost, cnt;
        cin >> id >> cost >> cnt;
        w[id] = cost;
        while (cnt -- )
        {
            int ver;
            cin >> ver;
            add(id, ver);
            st[ver] = true;
        }
    }

    int root = 1;
    while (st[root]) root ++ ;

    dfs(root);

    cout << min(f[root][1], f[root][2]) << endl;

    return 0;
}


數位DP

數位dp是一種計數用的dp,一般就是要統計一個區間[le,ri]內滿足一些條件數的個數。所謂數位dp,字面意思就是在數位上進行dp咯。數位還算是比較好聽的名字,數位的含義:一個數有個位、十位、百位、千位......數的每一位就是數位啦!
之所以要引入數位的概念完全就是為了dp。數位dp的實質就是換一種暴力列舉的方式,使得新的列舉方式滿足dp的性質,然後記憶化就可以了。

兩種不同的列舉:對於一個求區間[le,ri]滿足條件數的個數,最簡單的暴力如下:

for(int i=le;i<=ri;i++)
        if(right(i)) ans++;

然而這樣列舉不方便記憶化,或者說根本無狀態可言。
新的列舉:控制上界列舉,從最高位開始往下列舉,例如:ri=213,那麼我們從百位開始列舉:百位可能的情況有0,1,2(覺得這裡列舉0有問題的繼續看)

然後每一位列舉都不能讓列舉的這個數超過上界213(下界就是0或者1,這個次要),當百位枚舉了1,那麼十位列舉就是從0到9,因為百位1已經比上界2小了,後面數位列舉什麼都不可能超過上界。所以問題就在於:當高位列舉剛好達到上界是,那麼緊接著的一位列舉就有上界限制了。具體的這裡如果百位枚舉了2,那麼十位的列舉情況就是0到1,如果前兩位枚舉了21,最後一位之是0到3(這一點正好對於程式碼模板裡的一個變數limit 專門用來判斷列舉範圍)。最後一個問題:最高位列舉0:百位列舉0,相當於此時我列舉的這個數最多是兩位數,如果十位繼續列舉0,那麼我列舉的就是以為數咯,因為我們要列舉的是小於等於ri的所以數,當然不能少了位數比ri小的咯!(這樣列舉是為了無遺漏的列舉,不過可能會帶來一個問題,就是前導零的問題,模板裡用lead變量表示,不過這個不是每個題目都是會有影響的,可能前導零不會影響我們計數,具體要看題目)

由於這種新的列舉只控制了上界所以我們的Main函式總是這樣:

int main()
{
    long long le,ri;
    while(~scanf("%lld%lld",&le,&ri))
        printf("%lld\n",solve(ri)-solve(le-1));
}

統計[1,ri]數量和[1,le-1],然後相減就是區間[le,ri]的數量了,這裡我寫的下界是1,其實0也行,反正相減後就沒了,注意題目中le的範圍都是大於等於1的(不然le=0,再減一就廢了)

一個基本的動態模板(先看後面的例題也行):dp思想,列舉到當前位置pos,狀態為state(這個就是根據題目來的,可能很多,畢竟dp千變萬化)的數量(既然是計數,dp值顯然是儲存滿足條件數的個數)

typedef long long ll;
int a[20];
ll dp[20][state];//不同題目狀態不同
ll dfs(int pos,/*state變數*/,bool lead/*前導零*/,bool limit/*數位上界變數*/)//不是每個題都要判斷前導零
{
    //遞迴邊界,既然是按位列舉,最低位是0,那麼pos==-1說明這個數我列舉完了
    if(pos==-1) return 1;/*這裡一般返回1,表示你列舉的這個數是合法的,那麼這裡就需要你在列舉時必須每一位都要滿足題目條件,也就是說當前列舉到pos位,一定要保證前面已經列舉的數位是合法的。不過具體題目不同或者寫法不同的話不一定要返回1 */
    //第二個就是記憶化(在此前可能不同題目還能有一些剪枝)
    if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
    /*常規寫法都是在沒有限制的條件記憶化,這裡與下面記錄狀態是對應,具體為什麼是有條件的記憶化後面會講*/
    int up=limit?a[pos]:9;//根據limit判斷列舉的上界up;這個的例子前面用213講過了
    ll ans=0;
    //開始計數
    for(int i=0;i<=up;i++)//列舉,然後把不同情況的個數加到ans就可以了
    {
        if() ...
        else if()...
        ans+=dfs(pos-1,/*狀態轉移*/,lead && i==0,limit && i==a[pos]) //最後兩個變數傳參都是這樣寫的
        /*這裡還算比較靈活,不過做幾個題就覺得這裡也是套路了
        大概就是說,我當前數位列舉的數是i,然後根據題目的約束條件分類討論
        去計算不同情況下的個數,還有要根據state變數來保證i的合法性,比如題目
        要求數位上不能有62連續出現,那麼就是state就是要儲存前一位pre,然後分類,
        前一位如果是6那麼這意味就不能是2,這裡一定要儲存列舉的這個數是合法*/
    }
    //計算完,記錄狀態
    if(!limit && !lead) dp[pos][state]=ans;
    /*這裡對應上面的記憶化,在一定條件下時記錄,保證一致性,當然如果約束條件不需要考慮lead,這裡就是lead就完全不用考慮了*/
    return ans;
}
ll solve(ll x)
{
    int pos=0;
    while(x)//把數位都分解出來
    {
        a[pos++]=x%10;//個人老是喜歡編號為[0,pos),看不慣的就按自己習慣來,反正注意數位邊界就行
        x/=10;
    }
    return dfs(pos-1/*從最高位開始列舉*/,/*一系列狀態 */,true,true);//剛開始最高位都是有限制並且有前導零的,顯然比最高位還要高的一位視為0嘛
}
int main()
{
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri))
    {
        //初始化dp陣列為-1,這裡還有更加優美的優化,後面講
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}

常用優化

memset(dp,-1,sizeof dp);放在多組資料外面。

這一點是一個數位特點,使用的條件是:約束條件是每個數自身的屬性,而與輸入無關。

具體的:上一題不要62和4,這個約束對每一個數都是確定的,就是說任意一個數滿不滿足這個約束都是確定,比如444這個數,它不滿足約束條件,不管你輸入的區間是多少你都無法改變這個數不滿足約束這個事實,這就是數自身的屬性(我們每組資料只是在區間計數而已,只能說你輸入的區間不包含444的話,我們就不把它統計在內,而無法改變任何事實)。

由此,我們儲存的狀態就可以一直用(注意還有要limit,不同區間是會影響數位在有限制條件下的上限的)

這點優化就不給具體題目了,這個還有進一步的擴充套件。不過說幾個我遇到的簡單的約束:

  • 1.求數位和是10的倍數的個數,這裡簡化為數位sum%10這個狀態,即dp[pos][sum]這裡10 是與多組無關的,所以可以memset優化,不過注意如果題目的模是輸入的話那就不能這樣了。

  • 2.求二進位制1的數量與0的數量相等的個數,這個也是數自身的屬性。

當然,相減也是個很好的優化方式!!!

程式碼實現

求給定區間 [X,Y] 中滿足下列條件的整數個數:這個數恰好等於 K 個互不相等的 B 的整數次冪之和。

例如,設 X=15,Y=20,K=2,B=2,則有且僅有下列三個數滿足題意:

17=24+20
18=24+21
20=24+22
輸入格式
第一行包含兩個整數 X 和 Y,接下來兩行包含整數 K 和 B。

輸出格式
只包含一個整數,表示滿足條件的數的個數。

資料範圍
1≤X≤Y≤231−1,
1≤K≤20,
2≤B≤10
輸入樣例:
15 20
2
2
輸出樣例:
3

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 35;

int K, B;
int f[N][N];

void init()
{
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) f[i][j] = 1;
            else f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
}

int dp(int n)
{
    if (!n) return 0;

    vector<int> nums;
    while (n) nums.push_back(n % B), n /= B;

    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        if (x)  // 求左邊分支中的數的個數
        {
            res += f[i][K - last];
            if (x > 1)
            {
                if (K - last - 1 >= 0) res += f[i][K - last - 1];
                break;
            }
            else
            {
                last ++ ;
                if (last > K) break;
            }
        }

        if (!i && last == K) res ++ ;   // 最右側分支上的方案
    }

    return res;
}

int main()
{
    init();

    int l, r;
    cin >> l >> r >> K >> B;

    cout << dp(r) - dp(l - 1) << endl;

    return 0;
}


科協裡最近很流行數字遊戲。

某人命名了一種不降數,這種數字必須滿足從左到右各位數字呈非下降關係,如 123,446。

現在大家決定玩一個遊戲,指定一個整數閉區間 [a,b],問這個區間內有多少個不降數。

輸入格式
輸入包含多組測試資料。

每組資料佔一行,包含兩個整數 a 和 b。

輸出格式
每行給出一組測試資料的答案,即 [a,b] 之間有多少不降數。

資料範圍
1≤a≤b≤231−1
輸入樣例:
1 9
1 19
輸出樣例:
9
18

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 15;

int f[N][N];    // f[i, j]表示一共有i位,且最高位填j的數的個數

void init()
{
    for (int i = 0; i <= 9; i ++ ) f[1][i] = 1;

    for (int i = 2; i < N; i ++ )
        for (int j = 0; j <= 9; j ++ )
            for (int k = j; k <= 9; k ++ )
                f[i][j] += f[i - 1][k];
}

int dp(int n)
{
    if (!n) return 1;

    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;

    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        for (int j = last; j < x; j ++ )
            res += f[i + 1][j];

        if (x < last) break;
        last = x;

        if (!i) res ++ ;
    }

    return res;
}

int main()
{
    init();

    int l, r;
    while (cin >> l >> r) cout << dp(r) - dp(l - 1) << endl;

    return 0;
}


Windy 定義了一種 Windy 數:不含前導零且相鄰兩個數字之差至少為 2 的正整數被稱為 Windy 數。

Windy 想知道,在 A 和 B 之間,包括 A 和 B,總共有多少個 Windy 數?

輸入格式
共一行,包含兩個整數 A 和 B。

輸出格式
輸出一個整數,表示答案。

資料範圍
1≤A≤B≤2×109
輸入樣例1:
1 10
輸出樣例1:
9
輸入樣例2:
25 50
輸出樣例2:
20

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 11;

int f[N][10];

void init()
{
    for (int i = 0; i <= 9; i ++ ) f[1][i] = 1;

    for (int i = 2; i < N; i ++ )
        for (int j = 0; j <= 9; j ++ )
            for (int k = 0; k <= 9; k ++ )
                if (abs(j - k) >= 2)
                    f[i][j] += f[i - 1][k];
}

int dp(int n)
{
    if (!n) return 0;

    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;

    int res = 0;
    int last = -2;
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        for (int j = i == nums.size() - 1; j < x; j ++ )
            if (abs(j - last) >= 2)
                res += f[i + 1][j];

        if (abs(x - last) >= 2) last = x;
        else break;

        if (!i) res ++ ;
    }

    // 特殊處理有前導零的數
    for (int i = 1; i < nums.size(); i ++ )
        for (int j = 1; j <= 9; j ++ )
            res += f[i][j];

    return res;
}

int main()
{
    init();

    int l, r;
    cin >> l >> r;
    cout << dp(r) - dp(l - 1) << endl;

    return 0;
}


由於科協裡最近真的很流行數字遊戲。

某人又命名了一種取模數,這種數字必須滿足各位數字之和 mod N 為 0。

現在大家又要玩遊戲了,指定一個整數閉區間 [a.b],問這個區間內有多少個取模數。

輸入格式
輸入包含多組測試資料,每組資料佔一行。

每組資料包含三個整數 a,b,N。

輸出格式
對於每個測試資料輸出一行結果,表示區間內各位數字和 mod N 為 0 的數的個數。

資料範圍
1≤a,b≤231−1,
1≤N<100
輸入樣例:
1 19 9
輸出樣例:
2
        
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 11, M = 110;

int P;
int f[N][10][M];

int mod(int x, int y)
{
    return (x % y + y) % y;
}

void init()
{
    memset(f, 0, sizeof f);

    for (int i = 0; i <= 9; i ++ ) f[1][i][i % P] ++ ;

    for (int i = 2; i < N; i ++ )
        for (int j = 0; j <= 9; j ++ )
            for (int k = 0; k < P; k ++ )
                for (int x = 0; x <= 9; x ++ )
                    f[i][j][k] += f[i - 1][x][mod(k - j, P)];
}

int dp(int n)
{
    if (!n) return 1;

    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;

    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        for (int j = 0; j < x; j ++ )
            res += f[i + 1][j][mod(-last, P)];

        last += x;

        if (!i && last % P == 0) res ++ ;
    }

    return res;
}

int main()
{
    int l, r;
    while (cin >> l >> r >> P)
    {
        init();

        cout << dp(r) - dp(l - 1) << endl;
    }

    return 0;
}


杭州人稱那些傻乎乎粘嗒嗒的人為 62(音:laoer)。

杭州交通管理局經常會擴充一些的士車牌照,新近出來一個好訊息,以後上牌照,不再含有不吉利的數字了,這樣一來,就可以消除個別的士司機和乘客的心理障礙,更安全地服務大眾。

不吉利的數字為所有含有 4 或 62 的號碼。例如:62315,73418,88914 都屬於不吉利號碼。但是,61152 雖然含有 6 和 2,但不是 連號,所以不屬於不吉利數字之列。

你的任務是,對於每次給出的一個牌照號區間 [n,m],推斷出交管局今後又要實際上給多少輛新的士車上牌照了。

輸入格式
輸入包含多組測試資料,每組資料佔一行。

每組資料包含一個整數對 n 和 m。

當輸入一行為“0 0”時,表示輸入結束。

輸出格式
對於每個整數對,輸出一個不含有不吉利數字的統計個數,該數值佔一行位置。

資料範圍
1≤n≤m≤109
輸入樣例:
1 100
0 0
輸出樣例:
80

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 35;

int f[N][10];

void init()
{
    for (int i = 0; i <= 9; i ++ )
        if (i != 4)
            f[1][i] = 1;

    for (int i = 1; i < N; i ++ )
        for (int j = 0; j <= 9; j ++ )
        {
            if (j == 4) continue;
            for (int k = 0; k <= 9; k ++ )
            {
                if (k == 4 || j == 6 && k == 2) continue;
                f[i][j] += f[i - 1][k];
            }
        }
}

int dp(int n)
{
    if (!n) return 1;

    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;

    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        for (int j = 0; j < x; j ++ )
        {
            if (j == 4 || last == 6 && j == 2) continue;
            res += f[i + 1][j];
        }

        if (x == 4 || last == 6 && x == 2) break;
        last = x;

        if (!i) res ++ ;
    }

    return res;
}

int main()
{
    init();

    int l, r;
    while (cin >> l >> r, l || r)
    {
        cout << dp(r) - dp(l - 1) << endl;
    }

    return 0;
}


單身!

依然單身!

吉哥依然單身!

DS 級碼農吉哥依然單身!

所以,他平生最恨情人節,不管是 214 還是 77,他都討厭!

吉哥觀察了 214 和 77 這兩個數,發現:

2+1+4=7
7+7=7×2
77=7×11
最終,他發現原來這一切歸根到底都是因為和 7 有關!

所以,他現在甚至討厭一切和 7 有關的數!

什麼樣的數和 7 有關呢?

如果一個整數符合下面三個條件之一,那麼我們就說這個整數和 7 有關:

整數中某一位是 7;
整數的每一位加起來的和是 7 的整數倍;
這個整數是 7 的整數倍。
現在問題來了:吉哥想知道在一定區間內和 7 無關的整數的平方和。

輸入格式
第一行包含整數 T,表示共有 T 組測試資料。

每組資料佔一行,包含兩個整數 L 和 R。

輸出格式
對於每組資料,請計算 [L,R] 中和 7 無關的數字的平方和,並將結果對 109+7 取模後輸出。

資料範圍
1≤T≤50,
1≤L≤R≤1018
輸入樣例:
3
1 9
10 11
17 17
輸出樣例:
236
221
0


#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 20, P = 1e9 + 7;

struct F
{
    int s0, s1, s2;
}f[N][10][7][7];

int power7[N], power9[N];

int mod(LL x, int y)
{
    return (x % y + y) % y;
}

void init()
{
    for (int i = 0; i <= 9; i ++ )
    {
        if (i == 7) continue;
        auto& v = f[1][i][i % 7][i % 7];
        v.s0 ++, v.s1 += i, v.s2 += i * i;
    }

    LL power = 10;
    for (int i = 2; i < N; i ++, power *= 10)
        for (int j = 0; j <= 9; j ++ )
        {
            if (j == 7) continue;
            for (int a = 0; a < 7; a ++ )
                for (int b = 0; b < 7; b ++ )
                    for (int k = 0; k <= 9; k ++ )
                    {
                        if (k == 7) continue;
                        auto &v1 = f[i][j][a][b], &v2 = f[i - 1][k][mod(a - j * power, 7)][mod(b - j, 7)];
                        v1.s0 = mod(v1.s0 + v2.s0, P);
                        v1.s1 = mod(v1.s1 + v2.s1 + j * (power % P) % P * v2.s0, P);
                        v1.s2 = mod(v1.s2 + j * j * (power % P) % P * (power % P) % P * v2.s0 + v2.s2 + 2 * j * power % P * v2.s1, P);
                    }
        }

    power7[0] = 1;
    for (int i = 1; i < N; i ++ ) power7[i] = power7[i - 1] * 10 % 7;

    power9[0] = 1;
    for (int i = 1; i < N; i ++ ) power9[i] = power9[i - 1] * 10ll % P;
}

F get(int i, int j, int a, int b)
{
    int s0 = 0, s1 = 0, s2 = 0;
    for (int x = 0; x < 7; x ++ )
        for (int y = 0; y < 7; y ++ )
            if (x != a && y != b)
            {
                auto v = f[i][j][x][y];
                s0 = (s0 + v.s0) % P;
                s1 = (s1 + v.s1) % P;
                s2 = (s2 + v.s2) % P;
            }
    return {s0, s1, s2};
}

int dp(LL n)
{
    if (!n) return 0;

    LL backup_n = n % P;
    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;

    int res = 0;
    LL last_a = 0, last_b = 0;
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        for (int j = 0; j < x; j ++ )
        {
            if (j == 7) continue;
            int a = mod(-last_a * power7[i + 1], 7);
            int b = mod(-last_b, 7);
            auto v = get(i + 1, j, a, b);
            res = mod(
                res + 
                (last_a % P) * (last_a % P) % P * power9[i + 1] % P * power9[i + 1] % P * v.s0 % P + 
                v.s2 + 
                2 * last_a % P * power9[i + 1] % P * v.s1,
            P);
        }

        if (x == 7) break;
        last_a = last_a * 10 + x;
        last_b += x;

        if (!i && last_a % 7 && last_b % 7) res = (res + backup_n * backup_n) % P;
    }

    return res;
}

int main()
{
    int T;
    cin >> T;

    init();

    while (T -- )
    {
        LL l, r;
        cin >> l >> r;
        cout << mod(dp(r) - dp(l - 1), P) << endl;
    }

    return 0;
}