首页 > 技术文章 > 【图论】差分约束

Tshaxz 2021-09-03 02:04 原文

差分约束

image

1. 求不等式组的可行解

image
对于以上不等式,差分约束可以得到一组可行解。

在最短路中,求完最短路后,对于每个从j->i可以看成一个不等式\(x_i \leq x_j + c\)
说通俗点就是给我们一个图,我们可以把每条边看成一个不等式,我们在这个图上求每个点到源点的最短距离,求完之后,每个边的不等式必然满足\(x_i \leq x_j + c\)
也就是说,任何一个最短路问题都可以变成一个差分约束问题。
反过来也一样,对于一个不等式组中的任何一个不等式来说,我们都可以把它看成是一个从j走到i,长度是\(c_k\)的一条边,然后在图上随便选择一个起点,求一下每个点到起点的最短距离,求完之后,也必然满足\(x_i \leq x_j + c_k\)。因此,每个差分约束问题都可以转化成图论的单源最短路问题。
image

因此,当我们想求某一组可行解的时候,我们可以把不等式组中的每一个不等式转化成一条边,然后在图上求某一个点的单源最短路径,求完之后得到的结果必然满足不等式组中的限制条件,所以我们也就可以得到一个可行解。

但是,我们往往不能任取一个源点,源点需要满足一定的条件。
源点需要满足的条件从源点出发,一定可以走到所有的边。
因为只有从源点出发可以到达的边才满足不等式\(x_i \leq x_j + c\)

这样的话,对于每一个不等式组,我们一般可以分成两步来做:
1.把每一个不等式\(x_i \leq x_j + c_k\) 变成从ji,长度是\(c_k\)的一条边。
2.找到一个可以走到所有边的源点(一般为虚拟(超级)源点)。
3.从这个源点求一遍单源最短路。

当然,这里也有个问题,并不是所有的图都存在最短路,如果存在负环怎么办呢?
如果存在负环,还原到不等式中的结果就是会推出\(x_i \leq x_i +c\),因为负环,所以\(c\)是一个负数,一个数是不可能小于其本身加一个负数的,即出现\(x_i < x_i\),一个数小于其自己,这就出现了矛盾。

所以如果图中存在负环,则其转化成的不等式组一定存在\(x_i < x_i\)的矛盾,
反过来,不等式组中如果存在\(x_i < x_i\)的矛盾,则其转化成的图中一定存在负环。
所以不等式组无解等价于图存在负环
即:1.如果存在负环,则原不等式组一定无解。
2.如果不存在负环,则一定可以找到原不等式组的一组可行解。跑完单源最短路算法之后,dist[i]就是原不等式组的一个可行解。
在最短路问题中,无解等价于图存在负环
一图总结上文:
image

另外,上面是用最短路求可行解,最长路当然也是可以求可行解的。
观察可知:如果把不等式组的问题变成最短路的话,ji有一条长度为\(c_k\)可以变成\(x_i \leq x_j + c_k\),求完最短路后满足\(dist[i] \leq dist[j] + c_k\)
image

类比到最长路,求完最长路后,满足\(dist[i] \geq dist[j] + c_k\)
image
相当于是ij连了一条长度为\(-c_k\)的边。

相应的,在最长路问题中,无解就等价于图存在正环

当然,在求可行解的时候,用最短路或者最长路都是可以的。
用最短路求解:
将所有不等式转化成\(x_i \leq x_j + c_k\)的形式,然后从ji连一条长度为\(c_k\)的边。
用最长路求解:
将所有不等式转化成\(x_j \geq x_i - c_k\)的形式,然后从ij连一条长度为\(-c_k\)的边。
如果想连正边,也可以写成\(x_i \geq x_j + c_k\),然后从ji连一条长度为\(c_k\)的边。


之前刚学的时候还有一个疑问:
单源最短路不是:\(dist[j]>dist [i]+c\);则,\(dist[j]=dist[i] + c;\)吗?怎么理解 \(dist[y]<=dixt[x]+z\)?
答:正因为满足\(dist[j]>dist [i]+c\),所以跑完单源最短路算法后才会有:\(dist[y]<=dixt[x]+z\),因为不满足\(dist[y]<=dixt[x]+z\)就会更新它,让他满足\(dist[y]<=dixt[x]+z\)为止。


2. 如何求最大值或者最小值(实际题目中问的问题)

之前谈到了求可行解,通过把一个不等式组中的不等式转化成图中的边,找一个可以到达所有点的源点跑单源最短路算法,如果图中存在负环,则原不等式组无解,如果图中没有负环,则dist[i]即就是不等式组的一组可行解。
现在的问题是,我们如何求一个最小的可行解或者最大的可行解呢?

如果要求最小值或最大值的话,则不等式组中必然有一个不等式长成这样:\(x_i \geq c\)或者\(x_i \leq c\)\(c\)为常数。可以想一下,如果不等式组中的所有不等式都是变量形式的不等关系,那么整个不等式组都是一种相对的关系,但是如果不等式组内的某个不等式中的某一边全部为常数,则整个不等式组的状态就由一种不确定的相对关系转变到了确定的绝对关系,因为有了常数,经过条件的恒等变换,不等式组就一定大于等于或小于等于一个全部由常数构成的式子,那么这个全部由常数构成的式子的值就是不等式组的最值。

实际上也确实如此,算法题目中如果要让我们求最值,也一定会给出一个“\(x_i \geq c\)或者\(x_i \leq c\)\(c\)为常数”的式子,这样我们才能求出最值,不然都是相对关系。

PS:最短路给出的不等式为\(x_i \leq c\)
最长路给出的不等式为\(x_i \geq c\)

那么现在的问题就变成了:如何把“\(x_i \geq c\)或者\(x_i \leq c\)\(c\)为常数”的式子转化成图中的一条边呢?
方法:
我们以最短路的不等式\(x_i \leq c\)为例进行说明:
建立一个虚拟(又称超级)源点,通常为0号点,对于\(x_i \leq c\),可以转化成\(x_i \leq x_0 + c\)
,其中\(x_0\)是0,所以这个等式就可以转化为从0i有一条长度为 \(c\) 的边,问题解决。

另外,我们这里求的最大值最小值是指不等式组中每个变量的最大值最小值
\(x_1\)的最值,\(x_2\)的最值,...,以此类推。

比如说我们想求一下\(x_i\)的最大值,那么我们可以枚举一下所有\(x_i\)的链式关系,即找到所有与\(x_i\)相关的不等式,然后经过条件变换、放缩,最后一定可以转化成一个不等式,这个不等式的右边一定没有变量,只有常数。(否则的话都是相对关系,求不出来上限)
image
所以我们想求\(x_i\)的上界,其实就是看一下所有\(x_i\)构成的不等式链,求一下每个链的上界,然后在所有的上界中找出最小的那个上界,就是答案。

举个例子:比如说我们求出了
\(x_i \leq 5\)
\(x_i \leq 2\)
\(x_i \leq 3\)
那么\(x_i\)的最值应该取多少? 答案是2
因为只有\(x_i \leq 2\)才能满足所有的条件,并不是看到5最大所以5是最大值,5是最大值它满足不了所有条件。

而上述的每一个不等式链都可以转化为图论中的一条路径,即每条不等式链都是从0号点出发,走到i号点的一条路径。
求每个不等式链的上界就相当于求图论中每一条路径的最短路,然后在求出的所有最短路中找出最小的那个最短路,这就是我们要找的答案。
结合上面的例子,此时在所有最短路中找出的值最小的最短路就是在可以满足所有约束条件的情况下的最小上界

同理,比如说我们求出了
\(x_i \geq 5\)
\(x_i \geq 2\)
\(x_i \geq 3\)
那么满足条件的\(x_i\)应该取的值就是5
因为只有\(x_i \geq 5\)可以满足所有条件。
这里找的就是最大的下界。

综上:求最小值其实求的是在满足所有约束条件下最小的那个上界(即值最小的最短路)
最大值其实求的是在满足所有约束条件下最大的那个下界(即值最大的最长路)

当然这里的最小值“最小”的含义是最小的上界,“最小”对应最小上界中的“最小”一词,
也可以从最大值理解,最大值求的是最小上界,只不过这里的“最大”对应的是“上界”一词,具体用最大值来理解还是用最小值来理解,看个人习惯即可。但是一般市面上的算法书籍都是这样对应的:
最大值——最小上界——最短路
最小值——最大下界——最长路


3. 例题

下面拿几个例题来说明一下差分约束在题目中的具体操作方法:

在说例题之前,先说几点:
1.不等式组转化成的图可能含有负权边,即可能会有负环,所以要用SPFA算法判环
2.需要引入虚拟源点(超级源点),共n+1个点
3.最短路判负环,最长路判正环 SPFA算法中需要加一个cnt[]数组来记录当前访问过的结点数,如果在更新完某条边后结点数大于等于n + 1,则说明有环
4.循环队列实现的SPFA有时可能会超时,此时可以把循环队列改成栈试试。

OK,下面开始说例题。

AcWing 1169. 糖果
首先需要根据题目条件判断一下要求的是什么,题目中问至少需要多少个糖果,是一种大于等于号的关系,也就是说, 我们求出的答案\(x \geq c\),这是在求最大下界,或者借用习惯上的方法就是求最小值,用最长路。

那么用最长路在代码中的体现:

memset(dist, -0x3f, sizeof dist); //初始化dist数组时要初始化为负无穷
dist[j] < dist[t] + w[i]; ---> dist[j] = dist[t] + w[i]

然后因为不等式组可能无解,即图中有正环所以需要用SPFA算法判环
再根据本题题意,本题问的其实是每个不等式链取到等号的所有情况的和,即所有最长路的和。
跑完SPFA,如果没正环的话(最长路判正环)把dist[1]~dist[n]求个和输出即可。

还有就是需要根据题意列不等式组,因为是最长路,所以不等式要写成大于等于号的形式,即
\(x_i \geq x_j + c_k\),然后这就相当于从ji连一条长度为\(c_k\)的边。
image
当然,最重要的一点是不要忘记列出含常数的那一个不等式,这个不等式题目一般不会直接给出,需要自己从题意中提取。
后面建立虚拟源点的时候也是根据含常数的那一个不等式来建的。

C++代码:

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

using namespace std;

typedef long long LL;

const int N = 1e5 + 10, M = 3e5 + 10;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N],cnt[N], q[N];
bool st[N];

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

bool spfa()
{
    memset(dist, -0x3f, sizeof dist);
    dist[0] = 0;
    int hh = 0, tt = 1;
    q[0] = 0, st[0] = true;
    
    while(hh != tt)
    {
        int t = q[-- tt];
        st[t] = false;
        
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= n + 1) return true;
                if(!st[j])
                {
                    q[tt ++ ] = j;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for(int i = 0; i < m; i ++ )
    {
        int x, a, b;
        scanf("%d%d%d", &x, &a, &b);
        if(x == 1) add(b, a, 0), add(a, b, 0);
        else if(x == 2) add(a, b, 1);
        else if(x == 3) add(b, a, 0);
        else if(x == 4) add(b, a, 1);
        else if(x == 5) add(a, b, 0);
    }
    
    for(int i = 1; i <= n; i ++ ) add(0, i, 1);
    
    if(spfa()) puts("-1");
    else
    {
        LL res = 0;
        for(int i = 1; i <= n; i ++ )
        {
            res += dist[i];
        }
        printf("%lld\n", res);
    }
    
    return 0;
}

AcWing 362. 区间
先找不等式关系(这也是差分约束最难的地方)
可以利用前缀和的思想,但是前缀和的下标是从1开始的,题目的数据范围为0~50000,
所以需要把0这个位置空出来,让数据向右平移一个单位,变成1~50001,这个在我们读数据的时候+1即可。image
\(S_0 = 0\),设\(S_i\)表示在1~i中被选出数的个数,则\(S_{50001}\) 表示1~50001中被选出数的个数。
\(S_{50001}\)的最小值就是答案。此时你可能会问,“最小值”是怎么来的?
可以根据题目:题目问的是Z满足条件的整数不少于\(c\)个,什么叫“不少于”,不就是大于等于呗,大于等于号在前面已经讲过,是用来求所有下界中最大的那个下界的,即所有最长路中最长的那条。
或者用一般的方法:至少——最小值——最大下界——最长路。

在跑完SPFA后,如果不存在环,则dist[i]中存的就是源点到i的最长路的值(最大下界的值)

现在已经确定是最长路了,所以列出的不等关系应该是这样的形式:\(x_i \geq x_j + c_k\),即从ji连一条长度为\(c\)的边。
下面就是列不等式的过程了
1.\(S_i\)本身需要满足的性质:\(S_i \geq S_{i-1}\)\(1 \leq i \leq 50001\)
2.\(S_i - S_{i-1} \leq 1\)表示的是第\(i\)个数选没选,选了是1,没选是0(从\(S_i\)的含义理解)
但是因为要求最长路,需要写成大于等于的形式:\(S_{i-1} \geq S_i - 1\)
3.对于每个区间\([a,b]\)必须选\(c\)个,则有\(S_b - S_{a-1} \geq c\)
然后列完不等式要验算一下合法性,看一下条件够不够,然后还需要再看一下是否存在一个源点可以到达所有边,我们发现第一个条件\(S_i \geq S_{i-1}\)可以转换为i-1i有一条长度是0的边,所以这个条件就可以让0号点走到所有的边了。
image
C++代码:

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

using namespace std;

const int N = 50010, M = 150010;

int n;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], q[N]; //本题保证一定有解,即不存在环,所以不需要加cnt[]数组
bool st[N];
int s[N];

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

void spfa()
{
    memset(dist, -0x3f, sizeof dist); //最长路,初始化为负无穷
    dist[0] = 0;
    int hh = 0, tt = 1;
    q[0] = 0, st[0] = true;
    
    while(hh != tt)
    {
        int t = q[hh ++ ];
        if(hh == N) hh = 0;
        
        st[t] = false;
        
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[j] < dist[t] + w[i]) //最长路,这里是小于号
            {
                dist[j] = dist[t] + w[i];
                if(!st[j])
                {
                    q[tt ++ ] = j;
                    if(tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
}

int main()
{
    scanf("%d", &n);
    memset(h, -1, sizeof h);
    
    for(int i = 1; i <= 50001; i ++ )
    {
        add(i - 1, i, 0);
        add(i, i - 1, -1);
    }
    
    for(int i = 0; i < n; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        a ++, b ++ ; //因为要用前缀和,需要把0的位置空出来
        add(a - 1, b, c);
    }
    
    spfa(); //根据题意,一定有解,所以不需要判环
    
    printf("%d\n", dist[50001]);

    return 0;
}

AcWing 1170. 排队布局
本题还有扩展,以往的问题只需要判断是否有解或者求解的最值,但是本题还需判断在什么情况下1~n的距离可以任意大。
还是先找不等式关系,看最短路还是最长路,因为题目要求最大距离,所以这里求的是最大值——最小上界——最短路
所以本题列出的不等式关系应该是:\(x_i \leq x_j + c_k\)的形式
由题意:
\(x_b - x_a \leq L\)
\(x_b - x_a \geq D\)
将上面的条件转化成\(x_i \leq x_j + c_k\)的形式:
1.\(x_b \leq x_a + L\)ab连一条长度为\(L\)的边
2.\(x_a \leq x_b - D\)ba连一条长度为\(-D\)的边
\(x_i\)表示第i头牛的位置,则
3.\(x_i \leq x_{i+1}\)\(1 \leq i < n\) (这里不能取到n,因为\(x_{n+1}\)不存在)
至此就列出了三个不等式
检查一下题目中是否存在一个源点可以走到所有边,我们发现,第三个条件可以转化成从i+1i连一条长度为0的边,但是三个条件中不存在一个可以无条件到达所有点的源点。
所以我们需要建立一个虚拟源点(超级源点),称为0号点。
因为三个条件都是相对关系,所以我们可以假定所有\(x_i\)都是小于等于0的,即在0号点的左边,因为我们的目的是要从0号点向其他点连边,其他点在0号点左边就有\(x_i \leq x_0 + 0\)
这样就可以从0号点向其他所有点连一条0i的长度为0的边。这样就可以判断一下是否有环,有环就无解,无环就有解。

另外,我们在实现的时候不需要真的把超级源点(虚拟源点)建出来,因为在SPFA的第一步,0号点会把所有点更新到队列中去(因为一开始0号点距离为0,其他所有点都是正无穷),所以我们在做的时候不用真的把边建出来,直接把所有点加入到SPFA一开始的队列里就可以了,这和超级源点是等价的,因为建立了超级源点也会走到这一步,我们这里直接走到这一步还比较省空间。

到此第一问判断是否有解就解决了。

第二问:1~n之间的距离是否可以无限大?
由于题目中给出的都是相对关系,所以我们可以把1号点固定在一个绝对位置上。
比如我们可以把\(x_1\)规定为0,然后判断一下在这样的条件下,\(x_n\)是否可以无限大,这与题目的问题是等价的。
我们把\(x_1\)规定为0就相当于在不等式中引入了一个绝对关系,只要引入了绝对关系,就可以求出每一个点的最值。

我们这里的目标是判断一下1~n是否可以无限大,如果不可以无限大的话就求一下最大距离——最小上界——最短路。
这样我们跑一遍最短路算法,看一下\(x_n\)是多少,如果不是无穷,则\(x_n\)存的就是最大距离,如果是无穷那1~n就可以取到无穷大。

深入思考一下,1~n之间距离可以取无穷大反映到不等式链中就是\(x_1\)\(x_n\)之间不存在不等式链来约束它们,即\(x_1\)\(x_n\)之间不存在线性关系,所以可以它们之间的距离可以取无穷大。
C++代码:

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

using namespace std;

const int N = 1010, M = 20010, INF = 0x3f3f3f3f;

int n, m1, m2;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N], q[N];
bool st[N];

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

bool spfa(int size)
{
    //spfa要调用2次,所以每次调用要清空一下st,cnt,dist
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    memset(dist, 0x3f, sizeof dist); //最短路,初始化为正无穷
    int hh = 0, tt = 0;
    for(int i = 1; i <= size; i ++ )
    {
        dist[i] = 0;
        q[tt ++ ] = i;//将所有点加入队列
        if(tt == N) tt = 0;
        st[i] = true; 
    }

    while(hh != tt)
    {
        int t = q[hh ++ ];
        if(hh == N) hh = 0;

        st[t] = false;

        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[t] + w[i]) //最短路
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1; 
                if(cnt[j] >= n) return true; //判负环
                if(!st[j])
                {
                    q[tt ++ ] = j;
                    if(tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    scanf("%d%d%d", &n, &m1, &m2);
    memset(h, -1, sizeof h);
    while(m1 -- )
    {
        int a, b, c; 
        scanf("%d%d%d", &a, &b, &c);
        if(b < a) swap(a, b);//注意我们前面列不等式关系的时候是默认xb>xa的,而这里的输入不一定满足xb>xa,所以我们要特判一下
        add(a, b, c);
    }

    while(m2 -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        if(b < a) swap(a, b);
        add(b, a, -c);
    }

    for(int i = 1; i < n; i ++ ) add(i + 1, i, 0); 

    if(spfa(n)) puts("-1"); //第一问要把前n个点放进去判断有没有负环
    else
    {
        spfa(1); //只把第一个点放进去,然后看下dist[n]的结果。
        if(dist[n] == INF) puts("-2");
        else printf("%d\n", dist[n]);
    }
    return 0;
}

AcWing 393. 雇佣收银员
先看求最小值还是最大值,题目中问最少雇多少人,即最小值——最大下界——最长路
不等式关系形式为:\(x_i \geq x_j + c_k\)
首先可以先统计一下每个时刻来的人数,num[0]表示0点来的人数,...num[23]表示23点来的人数
\(x_0\)表示从num[0]中挑的人有多少个,\(x_1\)为从num[1]中挑的人有多少个...依次类推。
\(x_i\)表示从num[i]中挑的人有多少个,num[i]表示i点来的人数。
则关系式有:
1.\(0 \leq x_i \leq num[i]\)(从实际含义理解会发现很简单)
2.比如说一个人5点来,他可以服务到12点,所以有\(x_{i-7} + x_{i-6} + ...+x_i\geq r_i\)
但是我们发现上式不符合\(x_i \geq x_j + c_k\)这样的形式,而上式左边是一段和,所以自然而然地可以联想到前缀和,用前缀和来表示一段的和就会方便很多。
用前缀和的话就要把所有下标右移一位,把0空出来。
num[i]变为num[1]~num[24],
\(x_i\)也从\(x_1\)~\(x_{24}\)
然后用前缀和的表示形式替换掉上面列出的两个式子:
1.\(0 \leq S_i - S_{i-1} \leq num[i]\)\(1 \leq i \leq 24\)
2.第二个式子应该分段来看,因为1天24小时是循环的。
image
可以看到,从8以后算只有一段,但是小于8时再算就会分成两段(因为不够8小时)
\(i \geq 8\)时,\(S_i - S_{i-8} \geq r_i\)
\(0 \leq i \leq 7\)时,
可以找下规律,当i=7时,需要加上\(x_{23} = S_{24}-S_{23}\)
当i=6时,需要加上\(x_{22} + x_{23} = S_{24} - S_{22}\)
当i=5时,需要加上\(x_{21}+x_{22} + x_{23}=S_{24} - S_{21}\)
...
所以我们可以得出\(0 \leq i \leq 7\)时,有:
\(S_i + S_{24} - S_{i+16} \geq r_i\)
然后将上面得出的条件整理成\(x_i \geq x_j + c_k\)的形式:
(1)\(S_i \geq S_{i-1} + 0\) (前缀和的性质)
(2)\(S_{i-1} \geq S_i - num[i]\)
(3)\(i \geq 8\)\(S_i \geq S_{i-8} +r_i\)
(4)\(0 \leq i \leq 7\)\(S_i \geq S_{i+16} - S_{24} + r_i\)
然后我们发现前三个都满足\(x_i \geq x_j + c_k\)这样的形式,但最后一组不满足,因为最后一组有三个变量,这怎么处理呢?

其实我们可以不把\(S_{24}\)当成变量,直接枚举一下\(S_{24}\)的所有取值,因为只有1000个人,所以我们可以直接从0~1000枚举一下\(S_{24}\),这样对于每一个枚举的\(S_{24}\),它就是常量。

题目问的最终结果是所有收银员人数和,这个人数和就是\(S_{24}\)
所以我们可以从小到大枚举,找到第一个\(S_{24}\)的值使得问题有解,这个值就是满足条件的最小值。
如果枚举完所有的值都无解,那么问题就是无解的。
然后看一下是否存在超级源点,我们发现第一个条件是从i-1i连一条长度为0的边,可以到达所有的边。所以存在超级源点。
PS:其实第一个条件就是前缀和的自带条件,也就是说我们只要利用前缀和,就一定可以找到一个到达所有边的超级源点,并且这个点就是0号点。
C++代码:

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

using namespace std;

const int N = 30, M = 100;

int n;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N], q[N];
bool st[N];
int r[N], num[N]; //num为前缀和数组

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

void build(int s24)
{
    memset(h, -1, sizeof h);
    idx = 0;
    
    for(int i = 1; i <= 24; i ++ ) 
    {
        add(i - 1, i, 0);
        add(i, i - 1, -num[i]);
    }
    for(int i = 8; i <= 24; i ++ ) add(i - 8, i, r[i]);
    for(int i = 1; i <= 7; i ++ ) add(i + 16, i, r[i] - s24);
    add(0, 24, s24), add(24, 0, -s24);
}

bool spfa(int s24)
{
    build(s24);
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    memset(dist, -0x3f, sizeof dist);
    dist[0] = 0;
    int hh = 0, tt = 1;
    q[0] = 0, st[0] = true;
    
    while(hh != tt)
    {
        int t = q[hh ++ ];
        if(hh == N) hh = 0;
        
        st[t] = false;
        
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= 25) return true; //共25个点
                if(!st[j])
                {
                    q[tt ++ ] = j;
                    if(tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    int T;
    scanf("%d", &T);
    while(T -- )
    {
        for(int i = 1; i <= 24; i ++ ) scanf("%d", &r[i]);
        memset(num, 0, sizeof num); //每组数据清空前缀和数组
        scanf("%d", &n);
        for(int i = 0; i < n; i ++ )
        {
            int t;
            scanf("%d", &t);
            num[t + 1] ++ ; //右移一位,给前缀和空出0
        }
        
        bool success = false;
        for(int i = 1; i <= 1000; i ++ )
        {
            if(!spfa(i)) //如果没负环
            {
                printf("%d\n", i);
                success = true;
                break;
            }
        }
        if(!success) puts("No Solution"); //枚举完了还无解,那就无解
    }
    return 0;
}

推荐阅读