首页 > 解决方案 > 对列表求和,但仅适用于某个点之后的值

问题描述

我正在写一个网页来保存纸牌游戏的分数。到目前为止,让球员得分很容易,但有一个转折点。在任何一轮中,玩家的得分都可以在该轮开始时重置为零。我不想更改任何前几轮的分数,所以我只想在重置之后(包括)获得总和。玩家可能会在游戏中多次重置他们的分数,或者根本不会。

我可以通过查找最后一次(如果有的话)分数重置的多阶段过程来获得正确的分数,然后将所有手牌相加(或所有手牌没有重置) - 请参阅 PlayerGame.GetPlayerScore。

我仍在尝试了解使用 LINQ 做事的更复杂的方法,我想知道是否有一种方法可以使用单个 LINQ 语句来做到这一点?

最小代码:

class Program
{
    static void Main(string[] args)
    {
        PlayerGame playerGame = new PlayerGame();

        playerGame.PlayerHands = new List<PlayerHand>
        {
            new PlayerHand { Round = 1, Score = 10 },
            new PlayerHand { Round = 2, Score = 20 },
            new PlayerHand { Round = 3, Score = 30 },
            new PlayerHand { Round = 4, Score = 40, Reset = true },
            new PlayerHand { Round = 5, Score = 50 },
            new PlayerHand { Round = 6, Score = 60 }
        };

        Console.WriteLine($"Players score was {playerGame.GetPlayerScore()}");
        Console.ReadLine();
    }
}

class PlayerHand
{
    public int Round { get; set; }
    public int Score { get; set; }
    public bool Reset { get; set; } = false;
}

class PlayerGame
{
    public List<PlayerHand> PlayerHands { get; set; }

    public PlayerGame()
    {
        PlayerHands = new List<PlayerHand> { };
    }

    public int GetPlayerScore()
    {
        // Can all this be simplified to a single LINQ statement?
        var ResetIndex = PlayerHands.OrderBy(t => t.Round).LastOrDefault(t => t.Reset == true);

        if (ResetIndex != null)
        {
            return PlayerHands.Where(t => t.Round >= ResetIndex.Round).Sum(t => t.Score);
        }
        else
        {
            return PlayerHands.Sum(t => t.Score);
        }
    }
}

https://dotnetfiddle.net/s5rSqJ

如图所示,玩家的分数应该是 150。即分数在第 4 回合开始时重置,因此总分是第 4、5 和 6 回合的总和。

标签: c#linq

解决方案


总结几点,

  • 回合数是有限的(否则一场非常漫长的比赛!)。当我们在下面讨论反转时,这一观察很重要。
  • 回合已经按升序排序(根据评论),因此实际回合数无关紧要
  • 如果我们向后求和,我们不必扫描整个列表

因此,我们可以提出一个实现,即O(1)空间(就地,无分配)和O(n)时间(线性,小于重置时列表的大小)。

使用 MoreLinq

var score = hands.ReverseInPlace().TakeUntil(x => x.Reset).Sum(x => x.Score);

其中ReverseInPlace()以相反的顺序进行迭代,并且MoreEnumerable.TakeUntil()占用并包括具有真值Reset或序列结束的轮次。

ReverseInPlace将是一种扩展方法(IEnumerable<>如果需要,您可以推广到)。

public static class ListExtensions
{
    public static IEnumerable<T> ReverseInPlace<T>(this IList<T> source)
    {
        // add guard checks here, then do...
        for (int i=source.Length-1; i != -1; --i)
            yield return source[i];
    }
}

不使用 MoreLinq

您可以创建一个TakeInReverseUntil

public static IEnumerable<T> TakeInReverseUntil<T>(this IList<T> source, Func<T, bool> predicate)
{
    // add guard checks here, then do...
    for (int i=source.Length-1; i != -1; --i)
    {
        yield return source[i];
        if (predicate(source[i]) yield break;
    }
}

给你简化电话

var score = hands.TakeInReverseUntil(x => x.Reset).Sum(x => x.Score);

注意:Enumerable.Reverse()分配一个缓冲区,O(n)空间也是如此,这就是为什么我自己滚动ReverseInPlace来代替这个答案。


推荐阅读